- Accordion
- Alert
- Alert Dialog
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Calendar
- Card
- Checkbox
- Checkbox Group
- Collapsible
- Combobox
- Command
- Date Picker
- Dialog
- DrawerNew
- Empty
- Field
- Fieldset
- Form
- Frame
- Group
- Input
- Input Group
- Kbd
- Label
- Menu
- Meter
- Number Field
- OTP FieldNew
- Pagination
- Popover
- Preview Card
- Progress
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- Skeleton
- Slider
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Toolbar
- Tooltip
Table
A simple table component for displaying tabular data.
| Project | Status | Team | Budget |
|---|---|---|---|
| Website Redesign | Paid | Frontend Team | $12,500 |
| Mobile App | Unpaid | Mobile Team | $8,750 |
| API Integration | Pending | Backend Team | $5,200 |
| Database Migration | Paid | DevOps Team | $3,800 |
| User Dashboard | Paid | UX Team | $7,200 |
| Security Audit | Failed | Security Team | $2,100 |
| Total Budget | $39,550 | ||
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function Particle() {
return (
<Table>
<TableCaption>A list of current projects.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Status</TableHead>
<TableHead>Team</TableHead>
<TableHead className="text-right">Budget</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">Website Redesign</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>Frontend Team</TableCell>
<TableCell className="text-right">$12,500</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Mobile App</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-muted-foreground/64"
/>
Unpaid
</Badge>
</TableCell>
<TableCell>Mobile Team</TableCell>
<TableCell className="text-right">$8,750</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">API Integration</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-amber-500"
/>
Pending
</Badge>
</TableCell>
<TableCell>Backend Team</TableCell>
<TableCell className="text-right">$5,200</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Database Migration</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>DevOps Team</TableCell>
<TableCell className="text-right">$3,800</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">User Dashboard</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>UX Team</TableCell>
<TableCell className="text-right">$7,200</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Security Audit</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-red-500"
/>
Failed
</Badge>
</TableCell>
<TableCell>Security Team</TableCell>
<TableCell className="text-right">$2,100</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total Budget</TableCell>
<TableCell className="text-right">$39,550</TableCell>
</TableRow>
</TableFooter>
</Table>
);
}
Installation
pnpm dlx shadcn@latest add @coss/table
Usage
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"<Table>
<TableCaption>Caption</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Header</TableHead>
<TableHead>Header</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Cell</TableCell>
<TableCell>Cell</TableCell>
</TableRow>
</TableBody>
</Table>API Reference
Table
The main table container component. Set variant="card" for a card-style table: separated borders, rounded corners, and row surfaces that read as cards. That pairs well with a Frame (page chrome) or CardFrame (card shell with optional header actions). The default variant is a simpler row layout with standard borders.
| Prop | Type | Default |
|---|---|---|
className | string | |
variant | "default" | "card" | "default" |
<Table>
<TableHeader>...</TableHeader>
<TableBody>...</TableBody>
</Table><Table variant="card">
<TableHeader>...</TableHeader>
<TableBody>...</TableBody>
</Table>TableHeader
Header section of the table containing column headers.
| Prop | Type | Default |
|---|---|---|
className | string |
<TableHeader>
<TableRow>
<TableHead>Header</TableHead>
</TableRow>
</TableHeader>TableBody
Body section of the table containing table rows and data.
| Prop | Type | Default |
|---|---|---|
className | string |
<TableBody>
<TableRow>
<TableCell>Cell</TableCell>
</TableRow>
</TableBody>TableFooter
Footer section of the table.
| Prop | Type | Default |
|---|---|---|
className | string |
<TableFooter>
<TableRow>
<TableCell>Footer</TableCell>
</TableRow>
</TableFooter>TableRow
A row in the table.
| Prop | Type | Default |
|---|---|---|
className | string |
<TableRow>
<TableCell>Cell</TableCell>
</TableRow>TableHead
A header cell in the table.
| Prop | Type | Default |
|---|---|---|
className | string |
<TableHead>Header</TableHead>TableCell
A data cell in the table.
| Prop | Type | Default |
|---|---|---|
className | string |
<TableCell>Cell</TableCell>TableCaption
A caption for the table.
| Prop | Type | Default |
|---|---|---|
className | string |
<TableCaption>Caption</TableCaption>Examples
Card-style table
Use variant="card" when the grid itself should look like a set of cards (for example dashboards or standalone tables without an outer frame).
| Project | Status | Team | Budget |
|---|---|---|---|
| Website Redesign | Paid | Frontend Team | $12,500 |
| Mobile App | Unpaid | Mobile Team | $8,750 |
| API Integration | Pending | Backend Team | $5,200 |
| Database Migration | Paid | DevOps Team | $3,800 |
| User Dashboard | Paid | UX Team | $7,200 |
| Security Audit | Failed | Security Team | $2,100 |
| Total Budget | $39,550 | ||
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function Particle() {
return (
<Table className="w-full" variant="card">
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Status</TableHead>
<TableHead>Team</TableHead>
<TableHead className="text-right">Budget</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">Website Redesign</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>Frontend Team</TableCell>
<TableCell className="text-right">$12,500</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Mobile App</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-muted-foreground/64"
/>
Unpaid
</Badge>
</TableCell>
<TableCell>Mobile Team</TableCell>
<TableCell className="text-right">$8,750</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">API Integration</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-amber-500"
/>
Pending
</Badge>
</TableCell>
<TableCell>Backend Team</TableCell>
<TableCell className="text-right">$5,200</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Database Migration</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>DevOps Team</TableCell>
<TableCell className="text-right">$3,800</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">User Dashboard</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>UX Team</TableCell>
<TableCell className="text-right">$7,200</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Security Audit</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-red-500"
/>
Failed
</Badge>
</TableCell>
<TableCell>Security Team</TableCell>
<TableCell className="text-right">$2,100</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total Budget</TableCell>
<TableCell className="text-right">$39,550</TableCell>
</TableRow>
</TableFooter>
</Table>
);
}
Table in CardFrame
Put the table in CardFrame so the grid sits inside the card shell (border, radius, clipping). Use variant="card" on Table. The example below is static markup—no row selection or TanStack.
| Project | Status | Team | Budget |
|---|---|---|---|
| Website Redesign | Paid | Frontend Team | $12,500 |
| Mobile App | Unpaid | Mobile Team | $8,750 |
| API Integration | Pending | Backend Team | $5,200 |
| Database Migration | Paid | DevOps Team | $3,800 |
| User Dashboard | Paid | UX Team | $7,200 |
| Security Audit | Failed | Security Team | $2,100 |
| Total Budget | $39,550 | ||
import { Badge } from "@/components/ui/badge";
import { CardFrame } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function Particle() {
return (
<CardFrame className="w-full">
<Table variant="card">
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Status</TableHead>
<TableHead>Team</TableHead>
<TableHead className="text-right">Budget</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">Website Redesign</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>Frontend Team</TableCell>
<TableCell className="text-right">$12,500</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Mobile App</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-muted-foreground/64"
/>
Unpaid
</Badge>
</TableCell>
<TableCell>Mobile Team</TableCell>
<TableCell className="text-right">$8,750</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">API Integration</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-amber-500"
/>
Pending
</Badge>
</TableCell>
<TableCell>Backend Team</TableCell>
<TableCell className="text-right">$5,200</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Database Migration</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>DevOps Team</TableCell>
<TableCell className="text-right">$3,800</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">User Dashboard</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>UX Team</TableCell>
<TableCell className="text-right">$7,200</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Security Audit</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-red-500"
/>
Failed
</Badge>
</TableCell>
<TableCell>Security Team</TableCell>
<TableCell className="text-right">$2,100</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total Budget</TableCell>
<TableCell className="text-right">$39,550</TableCell>
</TableRow>
</TableFooter>
</Table>
</CardFrame>
);
}
Table in Frame
Wrap the table in a Frame for bordered app-surface framing. Use variant="card" on Table so rows keep the card-style treatment inside the frame.
| Project | Status | Team | Budget |
|---|---|---|---|
| Website Redesign | Paid | Frontend Team | $12,500 |
| Mobile App | Unpaid | Mobile Team | $8,750 |
| API Integration | Pending | Backend Team | $5,200 |
| Database Migration | Paid | DevOps Team | $3,800 |
| User Dashboard | Paid | UX Team | $7,200 |
| Security Audit | Failed | Security Team | $2,100 |
| Total Budget | $39,550 | ||
import { Badge } from "@/components/ui/badge";
import { Frame } from "@/components/ui/frame";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export default function Particle() {
return (
<Frame className="w-full">
<Table variant="card">
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Status</TableHead>
<TableHead>Team</TableHead>
<TableHead className="text-right">Budget</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">Website Redesign</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>Frontend Team</TableCell>
<TableCell className="text-right">$12,500</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Mobile App</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-muted-foreground/64"
/>
Unpaid
</Badge>
</TableCell>
<TableCell>Mobile Team</TableCell>
<TableCell className="text-right">$8,750</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">API Integration</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-amber-500"
/>
Pending
</Badge>
</TableCell>
<TableCell>Backend Team</TableCell>
<TableCell className="text-right">$5,200</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Database Migration</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>DevOps Team</TableCell>
<TableCell className="text-right">$3,800</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">User Dashboard</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-emerald-500"
/>
Paid
</Badge>
</TableCell>
<TableCell>UX Team</TableCell>
<TableCell className="text-right">$7,200</TableCell>
</TableRow>
<TableRow>
<TableCell className="font-medium">Security Audit</TableCell>
<TableCell>
<Badge variant="outline">
<span
aria-hidden="true"
className="size-1.5 rounded-full bg-red-500"
/>
Failed
</Badge>
</TableCell>
<TableCell>Security Team</TableCell>
<TableCell className="text-right">$2,100</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total Budget</TableCell>
<TableCell className="text-right">$39,550</TableCell>
</TableRow>
</TableFooter>
</Table>
</Frame>
);
}
Data table with TanStack
Use TanStack Table with variant="card" when you need column definitions, row selection, and flexRender over the same table primitives. Add @tanstack/react-table to your project for headless state (sorting, pagination, selection) beyond static markup.
| Project | Status | Team | Budget | |
|---|---|---|---|---|
Website Redesign | Paid | Frontend Team | $12,500 | |
Mobile App | Unpaid | Mobile Team | $8,750 | |
API Integration | Pending | Backend Team | $5,200 | |
Database Migration | Paid | DevOps Team | $3,800 | |
User Dashboard | Paid | UX Team | $7,200 | |
Security Audit | Failed | Security Team | $2,100 | |
| Total Budget | $39,550 | |||
"use client";
import {
type ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import type React from "react";
import { useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { CardFrame } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import {
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
type Project = {
id: string;
project: string;
status: "Paid" | "Unpaid" | "Pending" | "Failed";
team: string;
budget: number;
};
const data: Project[] = [
{
budget: 12500,
id: "1",
project: "Website Redesign",
status: "Paid",
team: "Frontend Team",
},
{
budget: 8750,
id: "2",
project: "Mobile App",
status: "Unpaid",
team: "Mobile Team",
},
{
budget: 5200,
id: "3",
project: "API Integration",
status: "Pending",
team: "Backend Team",
},
{
budget: 3800,
id: "4",
project: "Database Migration",
status: "Paid",
team: "DevOps Team",
},
{
budget: 7200,
id: "5",
project: "User Dashboard",
status: "Paid",
team: "UX Team",
},
{
budget: 2100,
id: "6",
project: "Security Audit",
status: "Failed",
team: "Security Team",
},
];
const getStatusColor = (status: Project["status"]) => {
switch (status) {
case "Paid":
return "bg-emerald-500";
case "Unpaid":
return "bg-muted-foreground/64";
case "Pending":
return "bg-amber-500";
case "Failed":
return "bg-red-500";
default:
return "bg-muted-foreground/64";
}
};
const getColumns = (): ColumnDef<Project>[] => [
{
cell: ({ row }) => {
const toggleHandler = row.getToggleSelectedHandler();
return (
<Checkbox
aria-label="Select row"
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
onCheckedChange={(value) => {
// Create a synthetic event for the handler
const syntheticEvent = {
target: { checked: !!value },
} as unknown as React.ChangeEvent<HTMLInputElement>;
toggleHandler(syntheticEvent);
}}
/>
);
},
enableSorting: false,
header: ({ table }) => {
const isAllSelected = table.getIsAllPageRowsSelected();
const isSomeSelected = table.getIsSomePageRowsSelected();
const toggleHandler = table.getToggleAllPageRowsSelectedHandler();
return (
<Checkbox
aria-label="Select all"
checked={isAllSelected}
indeterminate={isSomeSelected && !isAllSelected}
onCheckedChange={(value) => {
// Create a synthetic event for the handler
const syntheticEvent = {
target: { checked: !!value },
} as unknown as React.ChangeEvent<HTMLInputElement>;
toggleHandler(syntheticEvent);
}}
/>
);
},
id: "select",
},
{
accessorKey: "project",
cell: ({ row }) => (
<div className="font-medium">{row.getValue("project")}</div>
),
header: "Project",
},
{
accessorKey: "status",
cell: ({ row }) => {
const status = row.getValue("status") as Project["status"];
return (
<Badge variant="outline">
<span
aria-hidden="true"
className={`size-1.5 rounded-full ${getStatusColor(status)}`}
/>
{status}
</Badge>
);
},
header: "Status",
},
{
accessorKey: "team",
header: "Team",
},
{
accessorKey: "budget",
cell: ({ row }) => {
const amount = Number.parseFloat(row.getValue("budget"));
const formatted = new Intl.NumberFormat("en-US", {
currency: "USD",
maximumFractionDigits: 0,
minimumFractionDigits: 0,
style: "currency",
}).format(amount);
return <div className="text-right">{formatted}</div>;
},
header: () => <div className="text-right">Budget</div>,
},
];
export default function Particle() {
const [tableData] = useState<Project[]>(data);
const [rowSelection, setRowSelection] = useState({});
const columns = useMemo(() => getColumns(), []);
const table = useReactTable({
columns,
data: tableData,
enableRowSelection: true,
getCoreRowModel: getCoreRowModel(),
onRowSelectionChange: setRowSelection,
state: {
rowSelection,
},
});
const totalBudget = tableData.reduce(
(sum, project) => sum + project.budget,
0,
);
const formattedTotal = new Intl.NumberFormat("en-US", {
currency: "USD",
maximumFractionDigits: 0,
minimumFractionDigits: 0,
style: "currency",
}).format(totalBudget);
return (
<CardFrame className="w-full">
<Table variant="card">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
data-state={row.getIsSelected() && "selected"}
key={row.id}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell className="h-24 text-center" colSpan={columns.length}>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4}>Total Budget</TableCell>
<TableCell className="text-right">{formattedTotal}</TableCell>
</TableRow>
</TableFooter>
</Table>
</CardFrame>
);
}
Changelog
- Apr 12, 2026 —
Tableadds optionalvariant(defaultorcard)