Table
Rows and columns of records under a header row, scrolling sideways with a fade when wider than its box.
ui.table
components/ui/table.tsxUse case
Use a table when each row is one record and the columns compare the same facts: invoices by number, client, status and amount, orders by date and total, clients by city and balance. Status cells hold a Badge and amounts sit right-aligned in tabular figures. Wrap it in a rounded white box with overflow-hidden for its frame.
Use it when
- A list of invoices, orders or clients with the same facts per row
- Figures a person compares down a column
- Rows that open a record when clicked
- A short summary with a totals row (TableFooter)
Use something else when
- One record's facts on its own page: Card
- Moving through a long list page by page, under the table: Pagination
- The rows are still loading: Skeleton
- There are no rows yet: Empty state
Examples
| Invoice | Client | Status | Amount |
|---|---|---|---|
| INV-2026-031 | Bistro Le Marchand | $4,885.00 | |
| INV-2026-032 | Hôtel Rive-Nord | $1,240.00 | |
| INV-2026-033 | Clinique Belvédère | $860.00 | |
| INV-2026-034 | Brasserie La Voûte | $2,100.00 |
Code
Import
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"Usage
const TONE = { Paid: "success", "Due soon": "warning", Overdue: "error", Draft: "secondary" } as const
type Row = { number: string; client: string; status: keyof typeof TONE; amount: string }
export function InvoicesTable({ rows }: { rows: Row[] }) {
return (
<div className="overflow-hidden rounded-[20px] bg-background shadow-(--shadow-rest)">
<Table>
<TableHeader>
<TableRow>
<TableHead className="ps-5">Invoice</TableHead>
<TableHead>Client</TableHead>
<TableHead>Status</TableHead>
<TableHead className="pe-5 text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => (
<TableRow key={row.number}>
<TableCell className="ps-5 font-medium tabular-nums">{row.number}</TableCell>
<TableCell className="text-ink-2">{row.client}</TableCell>
<TableCell>
<Badge variant={TONE[row.status]}>{row.status}</Badge>
</TableCell>
<TableCell className="pe-5 text-right tabular-nums">{row.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}Rules
- The frame goes on a wrapper (rounded-[20px], overflow-hidden, bg-background, the rest shadow), never on Table: its container scrolls sideways with scroll-fade-x, and the fade would erase the frame.
- Numbers are tabular-nums, and amounts are right-aligned (text-right on the head and on the cell).
- Inside the frame the first column takes ps-5 and the last pe-5.
- A status is a Badge, its variant mapped from one TONE object.
- Headers are short and in sentence case (Invoice, Client, Status, Amount); a selected row carries data-state="selected".