Styling wrapper over native HTML table elements — no Radix, no built-in sorting/filtering/pagination. Pair with @tanstack/react-table (or similar) for that.
npx chandra-asri-ui add table| Invoice | Status | Amount |
|---|---|---|
| INV001 | Paid | $250.00 |
| INV002 | Pending | $150.00 |
| INV003 | Unpaid | $350.00 |
import { Badge } from "@/app/components/ui/badge";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/app/components/ui/table";
const invoices = [
{ id: "INV001", status: "green", label: "Paid", amount: "$250.00" },
{ id: "INV002", status: "yellow", label: "Pending", amount: "$150.00" },
{ id: "INV003", status: "red", label: "Unpaid", amount: "$350.00" },
] as const;
export function TableDemo() {
return (
<Table>
<TableCaption>A list of recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell className="font-medium">{invoice.id}</TableCell>
<TableCell>
<Badge variant={invoice.status} size="sm">
{invoice.label}
</Badge>
</TableCell>
<TableCell className="text-right">{invoice.amount}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
Table wraps itself in <div className="relative w-full overflow-auto"> — a wide table on a narrow screen scrolls horizontally on its own instead of breaking the page layout. TableRow has a hover highlight built in, plus a data-[state=selected] style for row-selection UIs — that attribute isn't set automatically, whatever's driving selection (e.g. `@tanstack/react-table`) has to set it. TableHead/TableCell both auto-remove their right padding when a cell contains a role="checkbox" element, so a select-all/select-row checkbox column sits flush against the edge.
import { cn } from "@/lib/utils";
import * as React from "react";
/**
* Plain wrapper styling over native HTML table elements — no Radix, no
* built-in sorting/filtering/pagination. Pair with `@tanstack/react-table`
* (or similar) at the call site for that; `TableRow`'s
* `data-[state=selected]` styling is ready for it but row-selection state
* itself has to be managed and set by whatever's driving the table.
*
* `hover:bg-surface-alt` / `data-[state=selected]:bg-accent/10` replace the
* source's `hover:bg-muted/50` / `data-[state=selected]:bg-muted` — this
* bundle's `--muted` token is muted *text*, not a background, so those are
* swapped for the closest actual background tokens (and given two visually
* distinct treatments, since both can apply to the same row at once).
*/
export const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
));
Table.displayName = "Table";
export const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
));
TableHeader.displayName = "TableHeader";
export const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
));
TableBody.displayName = "TableBody";
export const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"bg-surface-alt border-t font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
));
TableFooter.displayName = "TableFooter";
export const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-border hover:bg-surface-alt data-[state=selected]:bg-accent/10 border-b transition-colors",
className,
)}
{...props}
/>
));
TableRow.displayName = "TableRow";
export const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"text-muted h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
));
TableHead.displayName = "TableHead";
export const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className,
)}
{...props}
/>
));
TableCell.displayName = "TableCell";
export const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("text-muted mt-4 text-sm", className)}
{...props}
/>
));
TableCaption.displayName = "TableCaption";