Examples
Semantic table
Compound parts preserve caption, headers, rows, and cells as native table elements.
Semantic Table preview
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@phuctech/ui/components/table";
const rows = [
["Atlas onboarding", "Mai Nguyen", "Active"],
["Orbit billing", "Phuc Tran", "Invited"],
];
export function Example() {
return (
<div className="scrollbar-hover-reveal w-full overflow-x-auto rounded-lg border border-border">
<Table size="md" className="min-w-[34rem]">
<TableCaption>Projects and their current owners.</TableCaption>
<TableHeader><TableRow><TableHead>Project</TableHead><TableHead>Owner</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
<TableBody>{rows.map(([project, owner, status]) => <TableRow key={project}><TableCell className="font-medium">{project}</TableCell><TableCell>{owner}</TableCell><TableCell>{status}</TableCell></TableRow>)}</TableBody>
</Table>
</div>
);
}Density and sticky header
Use sm, md, or lg for row height and keep the header visible while the body scrolls.
Table small, default, and large density with sticky header
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@phuctech/ui/components/table";
const rows = [
["Atlas onboarding", "Mai Nguyen", "Active"],
["Orbit billing", "Phuc Tran", "Invited"],
];
function DensityTable({ size, label }: { size: "sm" | "md" | "lg"; label: string }) {
return (
<div className="scrollbar-hover-reveal w-full overflow-x-auto rounded-lg border border-border">
<Table size={size} className="min-w-[34rem]">
<TableHeader sticky><TableRow><TableHead>{label}</TableHead><TableHead>Owner</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
<TableBody>{rows.map(([project, owner, status]) => <TableRow key={project}><TableCell className="font-medium">{project}</TableCell><TableCell>{owner}</TableCell><TableCell>{status}</TableCell></TableRow>)}</TableBody>
</Table>
</div>
);
}
export function Example() {
return (
<div className="grid gap-4">
<DensityTable size="sm" label="Compact" />
<DensityTable size="md" label="Default" />
<DensityTable size="lg" label="Comfortable" />
</div>
);
}
Numeric alignment
Right-align numeric values while keeping the row and column meaning explicit.
| Month | Events | Change |
|---|---|---|
| August | 18,420 | +12% |
| July | 16,445 | — |
Table with numeric columns
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@phuctech/ui/components/table";
export function Example() {
return (
<Table>
<TableHeader><TableRow><TableHead>Month</TableHead><TableHead className="text-right">Events</TableHead><TableHead className="text-right">Change</TableHead></TableRow></TableHeader>
<TableBody>
<TableRow><TableCell>August</TableCell><TableCell className="text-right tabular-nums">18,420</TableCell><TableCell className="text-right tabular-nums text-[color:var(--color-success-text)]">+12%</TableCell></TableRow>
<TableRow><TableCell>July</TableCell><TableCell className="text-right tabular-nums">16,445</TableCell><TableCell className="text-right tabular-nums">—</TableCell></TableRow>
</TableBody>
</Table>
);
}
Loading, error, and empty
Request states stay inside the table boundary and explain the next action.
| Project | Status |
|---|---|
| Loading projects… | |
The project list could not load. Try again.
| Project | Status |
|---|---|
No projects yet Create a project to see it here. | |
Table loading, error, and empty states
import { Button } from "@phuctech/ui/components/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@phuctech/ui/components/table";
export function Example() {
return (
<div className="grid gap-4">
<Table aria-busy="true"><TableHeader><TableRow><TableHead>Project</TableHead><TableHead>Status</TableHead></TableRow></TableHeader><TableBody><TableRow><TableCell colSpan={2} className="py-8 text-center text-muted-foreground">Loading projects…</TableCell></TableRow></TableBody></Table>
<div className="grid gap-2 rounded-lg border border-[color:var(--color-danger-border)] bg-[color:var(--color-danger-bg)] p-4"><p role="alert" className="text-sm">The project list could not load.</p><Button size="sm" variant="outline">Try again</Button></div>
<Table><TableHeader><TableRow><TableHead>Project</TableHead><TableHead>Status</TableHead></TableRow></TableHeader><TableBody><TableRow><TableCell colSpan={2} className="py-8 text-center"><p className="font-medium">No projects yet</p><p className="text-sm text-muted-foreground">Create a project to see it here.</p></TableCell></TableRow></TableBody></Table>
</div>
);
}
Row selection
Checkboxes can compose with Table while select-all remains scoped to visible rows.
| Project | |
|---|---|
| Atlas onboarding | |
| Orbit billing | |
| Lumen research |
Table with stable-ID row selection
"use client";
import { useState } from "react";
import { Checkbox } from "@phuctech/ui/components/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@phuctech/ui/components/table";
const rows = [{ id: "atlas", name: "Atlas onboarding" }, { id: "orbit", name: "Orbit billing" }, { id: "lumen", name: "Lumen research" }];
export function Example() {
const [selected, setSelected] = useState<string[]>(["atlas"]);
const allSelected = selected.length === rows.length;
const partiallySelected = selected.length > 0 && !allSelected;
return (
<Table>
<TableHeader><TableRow selected={partiallySelected || allSelected}><TableHead className="w-12"><Checkbox aria-label="Select all visible projects" checked={allSelected} indeterminate={partiallySelected} onCheckedChange={(checked) => setSelected(checked ? rows.map((row) => row.id) : [])} /></TableHead><TableHead>Project</TableHead></TableRow></TableHeader>
<TableBody>{rows.map((row) => <TableRow key={row.id} selected={selected.includes(row.id)}><TableCell><Checkbox aria-label={`Select ${row.name}`} checked={selected.includes(row.id)} onCheckedChange={(checked) => setSelected((current) => checked ? [...current, row.id] : current.filter((id) => id !== row.id))} /></TableCell><TableCell className="font-medium">{row.name}</TableCell></TableRow>)}</TableBody>
</Table>
);
}
Wide and long content
A surrounding scroll region protects narrow screens without changing table semantics.
Wide Table inside a horizontal scroll region
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@phuctech/ui/components/table";
const rows = Array.from({ length: 6 }, (_, index) => ({ id: index, name: `Workspace record with a deliberately long label ${index + 1}`, owner: "A long owner name that remains readable" }));
export function Example() {
return (
<div className="scrollbar-hover-reveal max-w-full overflow-x-auto rounded-lg border border-border">
<Table className="min-w-[48rem]"><TableHeader><TableRow><TableHead>Workspace record</TableHead><TableHead>Owner</TableHead><TableHead>Last updated</TableHead></TableRow></TableHeader><TableBody>{rows.map((row) => <TableRow key={row.id}><TableCell className="max-w-72 whitespace-normal font-medium">{row.name}</TableCell><TableCell className="whitespace-normal">{row.owner}</TableCell><TableCell>Today</TableCell></TableRow>)}</TableBody></Table>
</div>
);
}
When to use
Use Table when rows and columns have stable relationships. Use a list or Card composition when each item is a standalone task or when the content does not compare naturally across columns.
Usage
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@phuctech/ui/components/table";
const rows = [
["Atlas onboarding", "Mai Nguyen", "Active"],
["Orbit billing", "Phuc Tran", "Invited"],
];
export function Example() {
return (
<div className="scrollbar-hover-reveal w-full overflow-x-auto rounded-lg border border-border">
<Table className="min-w-[34rem]">
<TableCaption>Projects and their current owners.</TableCaption>
<TableHeader><TableRow><TableHead>Project</TableHead><TableHead>Owner</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
<TableBody>{rows.map(([project, owner, status]) => <TableRow key={project}><TableCell className="font-medium">{project}</TableCell><TableCell>{owner}</TableCell><TableCell>{status}</TableCell></TableRow>)}</TableBody>
</Table>
</div>
);
}
The component parts render native table elements. Put the horizontal overflow boundary around the table in a narrow layout; do not replace table semantics with a grid of divs.
Composition
TableCaptionnames the data set for assistive technology.TableHeaddefaults toscope="col"; passscope="row"for row headers.TableRow selectedexposesdata-state="selected"for a selected row.- Use
colSpan,aria-sort, and other native attributes directly on the relevant parts.
API
Tokens
Accessibility
- Use a caption, column headers, and row headers where appropriate.
- Keep selection checkboxes labeled with the row identity and scope select-all to the visible set.
- Allow horizontal scrolling at narrow widths; do not clip long labels.
- Keep loading, error, and empty feedback inside a stable table boundary.
Related
- Data table composes Table with staged filters, sorting, paging, and selection.
- Checkbox supplies row selection controls.
- Layout & Grid documents wide-content boundaries.