- Accordion
- Alert
- Alert Dialog
- Attachment
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Checkbox
- Collapsible
- Command
- Context Menu
- Data Table
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input Group
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Pagination
- Popover
- Progress
- Questionnaire
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Unpaid | Bank transfer | $350.00 |
| INV004 | Paid | Credit card | $450.00 |
| INV005 | Paid | PayPal | $550.00 |
| INV006 | Pending | Bank transfer | $200.00 |
| INV007 | Unpaid | Credit card | $300.00 |
| Total | $2,250.00 | ||
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const invoices = [
{
invoice: "INV001",
status: "Paid",
method: "Credit card",
amount: "$250.00",
},
{ invoice: "INV002", status: "Pending", method: "PayPal", amount: "$150.00" },
{
invoice: "INV003",
status: "Unpaid",
method: "Bank transfer",
amount: "$350.00",
},
{
invoice: "INV004",
status: "Paid",
method: "Credit card",
amount: "$450.00",
},
{ invoice: "INV005", status: "Paid", method: "PayPal", amount: "$550.00" },
{
invoice: "INV006",
status: "Pending",
method: "Bank transfer",
amount: "$200.00",
},
{
invoice: "INV007",
status: "Unpaid",
method: "Credit card",
amount: "$300.00",
},
]
function StatusBadge({ status }: { status: string }) {
if (status === "Paid") {
return <Badge className="bg-success/15 text-success">Paid</Badge>
}
if (status === "Unpaid") {
return <Badge variant="destructive">Unpaid</Badge>
}
return <Badge variant="secondary">{status}</Badge>
}
export function TableDemo() {
return (
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.invoice}>
<TableCell className="font-medium">{invoice.invoice}</TableCell>
<TableCell>
<StatusBadge status={invoice.status} />
</TableCell>
<TableCell>{invoice.method}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={3}>Total</TableCell>
<TableCell className="text-right">$2,250.00</TableCell>
</TableRow>
</TableFooter>
</Table>
)
}Installation#
pnpm dlx shadcn@latest add https://ui.ikramhasan.com/r/table.json
Usage#
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="font-medium">INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit card</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
</TableBody>
</Table>The table is its own frame: the header, footer and caption sit on a muted shell, and the body rows on a raised card inside it. Don't wrap it in another bordered container.
Examples#
Actions#
A row action menu with a DropdownMenu. The row keeps its hover fill while the menu is open.
| Product | Price | Actions |
|---|---|---|
| Wireless mouse | $29.99 | |
| Mechanical keyboard | $129.99 | |
| USB-C hub | $49.99 |
"use client"
import { MoreHorizontalIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const products = [
{ name: "Wireless mouse", price: "$29.99" },
{ name: "Mechanical keyboard", price: "$129.99" },
{ name: "USB-C hub", price: "$49.99" },
]
export function TableActions() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Product</TableHead>
<TableHead>Price</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{products.map((product) => (
<TableRow key={product.name}>
<TableCell className="font-medium">{product.name}</TableCell>
<TableCell>{product.price}</TableCell>
<TableCell className="py-0 text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="icon-sm"
className="-mr-1.5"
/>
}
>
<MoreHorizontalIcon />
<span className="sr-only">Open menu</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}Selection#
Set data-state="selected" on a TableRow to tint it.
| Name | Role | ||
|---|---|---|---|
| Olivia Martin | olivia@example.com | Owner | |
| Jackson Lee | jackson@example.com | Admin | |
| Isabella Nguyen | isabella@example.com | Member | |
| William Kim | will@example.com | Member |
"use client"
import * as React from "react"
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
const members = [
{
id: "1",
name: "Olivia Martin",
email: "olivia@example.com",
role: "Owner",
},
{ id: "2", name: "Jackson Lee", email: "jackson@example.com", role: "Admin" },
{
id: "3",
name: "Isabella Nguyen",
email: "isabella@example.com",
role: "Member",
},
{ id: "4", name: "William Kim", email: "will@example.com", role: "Member" },
]
export function TableSelection() {
const [selected, setSelected] = React.useState<string[]>(["2"])
const allSelected = selected.length === members.length
return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-8">
<Checkbox
checked={allSelected}
onCheckedChange={(checked) =>
setSelected(checked ? members.map((member) => member.id) : [])
}
aria-label="Select all"
/>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead className="text-right">Role</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{members.map((member) => {
const isSelected = selected.includes(member.id)
return (
<TableRow
key={member.id}
data-state={isSelected ? "selected" : undefined}
>
<TableCell>
<Checkbox
checked={isSelected}
onCheckedChange={(checked) =>
setSelected((current) =>
checked
? [...current, member.id]
: current.filter((id) => id !== member.id)
)
}
aria-label={`Select ${member.name}`}
/>
</TableCell>
<TableCell className="font-medium">{member.name}</TableCell>
<TableCell className="text-muted-foreground">
{member.email}
</TableCell>
<TableCell className="text-right">
<Badge
variant={member.role === "Owner" ? "default" : "secondary"}
>
{member.role}
</Badge>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
)
}Key/value#
Without a TableHeader, the card fills the shell.
| Status | Connected |
| Products synced | 1,284 |
| Last sync | 2 minutes ago |
| Connected on | Mar 14, 2026 |
import {
CalendarIcon,
PackageIcon,
RefreshCwIcon,
ShieldCheckIcon,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"
const details = [
{ icon: ShieldCheckIcon, label: "Status", value: "Connected" },
{ icon: PackageIcon, label: "Products synced", value: "1,284" },
{ icon: RefreshCwIcon, label: "Last sync", value: "2 minutes ago" },
{ icon: CalendarIcon, label: "Connected on", value: "Mar 14, 2026" },
]
export function TableKeyValue() {
return (
<div className="w-full max-w-md">
<Table className="table-fixed">
<TableBody>
{details.map(({ icon: Icon, label, value }) => (
<TableRow key={label}>
<TableCell className="border-r text-muted-foreground">
<span className="flex items-center gap-2 [&>svg]:size-4">
<Icon />
{label}
</span>
</TableCell>
<TableCell>
{label === "Status" ? (
<Badge className="bg-success/15 text-success">{value}</Badge>
) : (
value
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}Data Table#
For sorting, filtering and pagination, build a data table with TanStack Table on top of these parts. See the Data Table guide.
API Reference#
Each part renders the HTML element of the same name and accepts all of its props: Table (table, inside a scrolling div with data-slot="table-container"), TableHeader (thead), TableBody (tbody), TableFooter (tfoot), TableRow (tr), TableHead (th), TableCell (td) and TableCaption (caption).
| Attribute | Part | Description |
|---|---|---|
data-state="selected" | TableRow | Tints the row as selected. |