| Invoice | Customer | Status | Issued | Amount |
|---|---|---|---|---|
| INV-1042 | Acme Corp | Paid | Mar 2 | $6,300.00 |
| INV-1043 | Bluebird Studio | Sent | Mar 5 | $1,800.00 |
| INV-1044 | Cedar & Pine | Pending | Mar 9 | $1,275.00 |
| INV-1045 | Driftwood Labs | Overdue | Feb 14 | $4,500.00 |
| INV-1046 | Evergreen Co. | Draft | Mar 12 | $600.00 |
| Total | $14,475.00 | |||
import { Status } from "@/components/ui/status";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const statuses = {
paid: { label: "Paid", variant: "success" },
sent: { label: "Sent", variant: "info" },
pending: { label: "Pending", variant: "warning" },
overdue: { label: "Overdue", variant: "destructive" },
draft: { label: "Draft", variant: "neutral" },
} as const;
const invoices = [
{ id: "INV-1042", customer: "Acme Corp", status: "paid", issued: "Mar 2", amount: 6300 },
{ id: "INV-1043", customer: "Bluebird Studio", status: "sent", issued: "Mar 5", amount: 1800 },
{ id: "INV-1044", customer: "Cedar & Pine", status: "pending", issued: "Mar 9", amount: 1275 },
{ id: "INV-1045", customer: "Driftwood Labs", status: "overdue", issued: "Feb 14", amount: 4500 },
{ id: "INV-1046", customer: "Evergreen Co.", status: "draft", issued: "Mar 12", amount: 600 },
] as const;
const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
export default function TableInvoices() {
const total = invoices.reduce((sum, invoice) => sum + invoice.amount, 0);
return (
<Table>
<TableCaption>Invoices issued this quarter.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Customer</TableHead>
<TableHead>Status</TableHead>
<TableHead>Issued</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell className="font-mono">{invoice.id}</TableCell>
<TableCell className="font-medium">{invoice.customer}</TableCell>
<TableCell>
<Status variant={statuses[invoice.status].variant}>
{statuses[invoice.status].label}
</Status>
</TableCell>
<TableCell className="text-muted-foreground">{invoice.issued}</TableCell>
<TableCell className="text-right">{currency.format(invoice.amount)}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={4}>Total</TableCell>
<TableCell className="text-right text-foreground">{currency.format(total)}</TableCell>
</TableRow>
</TableFooter>
</Table>
);
}- Add the registryOnce per project
pnpm dlx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json - Add table
pnpm dlx shadcn@latest add @jarvis/table
Usage
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
export function InvoiceTable() {
return (
<Table>
<TableCaption>Invoices issued this quarter.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV-1042</TableCell>
<TableCell className="text-right">$6,300.00</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell>Total</TableCell>
<TableCell className="text-right">$6,300.00</TableCell>
</TableRow>
</TableFooter>
</Table>
);
}Wide tables scroll sideways inside a Scroll Area, which fades the edges.
Composition
Examples
Row selection
Set data-state="selected" on a selected row to highlight it. Give each checkbox an aria-label
that names its row.
| Name | Role | ||
|---|---|---|---|
| Ada Park | ada@example.com | Owner | |
| Sam Rivera | sam@example.com | Admin | |
| Priya Shah | priya@example.com | Member | |
| Jonah Kim | jonah@example.com | Member |
"use client";
import * as React from "react";
import { Checkbox } from "@/components/ui/checkbox";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const members = [
{ id: "ada", name: "Ada Park", email: "ada@example.com", role: "Owner" },
{ id: "sam", name: "Sam Rivera", email: "sam@example.com", role: "Admin" },
{ id: "priya", name: "Priya Shah", email: "priya@example.com", role: "Member" },
{ id: "jonah", name: "Jonah Kim", email: "jonah@example.com", role: "Member" },
];
export default function TableSelection() {
const [selected, setSelected] = React.useState<string[]>(["sam"]);
const all = selected.length === members.length;
return (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-8">
<Checkbox
aria-label="Select all members"
checked={all}
indeterminate={selected.length > 0 && !all}
onCheckedChange={(checked) =>
setSelected(checked ? members.map((member) => member.id) : [])
}
/>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>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
aria-label={`Select ${member.name}`}
checked={isSelected}
onCheckedChange={(checked) =>
setSelected((current) =>
checked ? [...current, member.id] : current.filter((id) => id !== member.id),
)
}
/>
</TableCell>
<TableCell className="font-medium">{member.name}</TableCell>
<TableCell className="text-muted-foreground">{member.email}</TableCell>
<TableCell>{member.role}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
}Row actions
Give the column's header visually hidden text, and each menu trigger an aria-label that names
its row.
| Invoice | Customer | Amount | Actions |
|---|---|---|---|
| INV-1042 | Acme Corp | $6,300.00 | |
| INV-1043 | Bluebird Studio | $1,800.00 | |
| INV-1044 | Cedar & Pine | $1,275.00 |
import { EllipsisIcon, FileTextIcon, UploadIcon } 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 invoices = [
{ id: "INV-1042", customer: "Acme Corp", amount: "$6,300.00" },
{ id: "INV-1043", customer: "Bluebird Studio", amount: "$1,800.00" },
{ id: "INV-1044", customer: "Cedar & Pine", amount: "$1,275.00" },
];
export default function TableRowActions() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Customer</TableHead>
<TableHead className="text-right">Amount</TableHead>
<TableHead className="w-10">
<span className="sr-only">Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell className="font-mono">{invoice.id}</TableCell>
<TableCell className="font-medium">{invoice.customer}</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
<TableCell className="pr-2 text-right">
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
size="icon-sm"
aria-label={`Actions for ${invoice.id}`}
/>
}
>
<EllipsisIcon />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<FileTextIcon />
View invoice
</DropdownMenuItem>
<DropdownMenuItem>
<UploadIcon />
Send reminder
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">Void invoice</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}Props
Table's props, including className, go to the <table>, not its scrolling wrapper.
| Part | Extends |
|---|---|
| Table | <table> |
| TableHeader | <thead> |
| TableBody | <tbody> |
| TableFooter | <tfoot> |
| TableRow | <tr> |
| TableHead | <th> |
| TableCell | <td> |
| TableCaption | <caption> |