Structured data table for rows, columns, and comparisons.
npx shadcn@latest add @iconiq/table"use client";
import { Search } from "lucide-react";
import { useMemo, useState } from "react";
import {
Table,
TableBody,
TableCaption,
TableCell,
TableEmpty,
TableHead,
TableHeader,
TableLoading,
TablePagination,
TableRow,
TableSortButton,
TableToolbar,
} from "@/components/ui/table";
type Row = {
id: string;
name: string;
role: string;
status: "Active" | "Pending" | "Archived";
amount: number;
};
type SortKey = keyof Pick<Row, "name" | "role" | "status" | "amount">;
const rows: Row[] = [
{ id: "1", name: "Ada Lovelace", role: "Engineer", status: "Active", amount: 4200 },
{ id: "2", name: "Alan Turing", role: "Researcher", status: "Active", amount: 5800 },
{ id: "3", name: "Grace Hopper", role: "Architect", status: "Pending", amount: 3100 },
];
const statusStyles: Record<Row["status"], string> = {
Active: "bg-foreground text-background",
Pending: "bg-muted text-foreground",
Archived: "border border-border bg-transparent text-muted-foreground",
};
export function TablePreview() {
const [query, setQuery] = useState("");
const [sort, setSort] = useState<{ key: SortKey; dir: "asc" | "desc" }>({
key: "name",
dir: "asc",
});
const [loading, setLoading] = useState(false);
const visible = useMemo(() => {
const filtered = rows.filter((row) =>
(row.name + row.role + row.status).toLowerCase().includes(query.toLowerCase())
);
return [...filtered].sort((a, b) => {
const av = a[sort.key];
const bv = b[sort.key];
if (av < bv) return sort.dir === "asc" ? -1 : 1;
if (av > bv) return sort.dir === "asc" ? 1 : -1;
return 0;
});
}, [query, sort]);
const toggleSort = (key: SortKey) =>
setSort((current) => ({
key,
dir: current.key === key && current.dir === "asc" ? "desc" : "asc",
}));
return (
<div className="w-full max-w-4xl">
<TableToolbar tableId="team-table">
<div className="relative max-w-sm flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<input
className="h-10 w-full border-b border-border bg-transparent pl-9 pr-3 text-sm outline-none transition-colors focus:border-foreground"
onChange={(event) => setQuery(event.target.value)}
placeholder="Search…"
value={query}
/>
</div>
<button
className="text-muted-foreground text-sm transition-colors hover:text-foreground"
onClick={() => setLoading((current) => !current)}
type="button"
>
{loading ? "Show data" : "Simulate loading"}
</button>
</TableToolbar>
<Table id="team-table">
<TableHeader>
<TableRow variant="header">
<TableHead sortDirection={sort.key === "name" ? sort.dir : "none"}>
<TableSortButton
active={sort.key === "name"}
direction={sort.dir}
onClick={() => toggleSort("name")}
>
Name
</TableSortButton>
</TableHead>
<TableHead sortDirection={sort.key === "role" ? sort.dir : "none"}>
<TableSortButton
active={sort.key === "role"}
direction={sort.dir}
onClick={() => toggleSort("role")}
>
Role
</TableSortButton>
</TableHead>
<TableHead sortDirection={sort.key === "status" ? sort.dir : "none"}>
<TableSortButton
active={sort.key === "status"}
direction={sort.dir}
onClick={() => toggleSort("status")}
>
Status
</TableSortButton>
</TableHead>
<TableHead align="right" sortDirection={sort.key === "amount" ? sort.dir : "none"}>
<TableSortButton
active={sort.key === "amount"}
align="right"
direction={sort.dir}
onClick={() => toggleSort("amount")}
>
Amount
</TableSortButton>
</TableHead>
</TableRow>
</TableHeader>
<TableBody className="min-h-[240px]">
{loading ? (
<TableLoading rows={3} />
) : (
<>
{visible.map((row, index) => (
<TableRow hoverable index={index} key={row.id}>
<TableCell className="font-medium text-foreground">{row.name}</TableCell>
<TableCell className="text-muted-foreground">{row.role}</TableCell>
<TableCell>
<span className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${statusStyles[row.status]}`}>
{row.status}
</span>
</TableCell>
<TableCell align="right" className="tabular-nums text-foreground">
${row.amount.toLocaleString()}
</TableCell>
</TableRow>
))}
{visible.length === 0 ? <TableEmpty>No results.</TableEmpty> : null}
</>
)}
</TableBody>
</Table>
<TablePagination
onPageChange={() => {}}
page={1}
pageCount={1}
pageSize={10}
totalItems={visible.length}
/>
</div>
);
}childrenCompose TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, and optional helper primitives inside the root.
Type ReactNode
columnsShared grid-template-columns value applied to every header and body row so the native table semantics still keep the custom grid layout aligned.
Type string·Default "minmax(0,1.4fr) minmax(0,1fr) minmax(0,1fr) minmax(0,1fr)"
sizeControls row density by tightening header and body cell padding across the table.
Type "default" | "compact"·Default default
stickyHeaderWhen true, TableHeader stays pinned while the surrounding scroll container moves.
Type boolean·Default false
classNameMerged onto the native table element when you need to adjust width, spacing, or placement.
Type string
childrenUsually a search field, actions, filters, or bulk controls placed above the table.
Type ReactNode
tableIdOptional id of the related table, exposed through aria-controls for toolbar controls.
Type string
classNameMerged onto the toolbar wrapper.
Type string
childrenUsually one header TableRow.
Type ReactNode
stickyOverrides the root stickyHeader setting for this header section.
Type boolean
classNameMerged onto the header wrapper.
Type string
childrenOne or more TableRow elements, plus optional TableEmpty or TableLoading when no rows are visible or data is still loading.
Type ReactNode
classNameMerged onto the body wrapper.
Type string
childrenUsually one footer TableRow containing TablePagination or summary cells.
Type ReactNode
classNameMerged onto the footer wrapper.
Type string
variantHeader rows skip mount and exit motion, while body rows keep the original motion defaults.
Type "header" | "body"·Default body
indexOptional row index used to apply a subtle stagger to body row entry motion.
Type number·Default 0
hoverableWhen true, body rows get the muted hover wash. Defaults to false so informational rows do not imply clickability.
Type boolean
selectedApplies selected styling and exposes data-state="selected" for active row selection.
Type boolean·Default false
classNameMerged onto the row shell for spacing or color overrides.
Type string
Motion tr propsAdditional motion.tr props such as layout, transition, whileHover, and exit can still be passed directly.
Type ComponentPropsWithoutRef<typeof motion.tr>
alignControls left, center, or right alignment for the header cell content.
Type "left" | "center" | "right"·Default left
sortDirectionExplicit aria-sort source for sortable columns. Pass "none" on inactive sortable headers.
Type "asc" | "desc" | "none"
childrenHeader label or a custom control such as TableSortButton.
Type ReactNode
classNameMerged onto the header cell wrapper.
Type string
alignControls left, center, or right alignment for the cell content.
Type "left" | "center" | "right"·Default left
childrenRendered cell content.
Type ReactNode
classNameMerged onto the cell wrapper.
Type string
checkedControlled checked state for the select-all checkbox.
Type boolean·Default false
indeterminateShows the mixed selection state when only some visible rows are selected.
Type boolean·Default false
onCheckedChangeCalled when the select-all checkbox toggles.
Type (checked: boolean) => void
aria-labelAccessible name for the select-all checkbox.
Type string·Default "Select all rows"
checkedControlled checked state for the row checkbox.
Type boolean·Default false
onCheckedChangeCalled when the row checkbox toggles.
Type (checked: boolean) => void
aria-labelAccessible name for the row checkbox, such as the row title.
Type string
childrenCaption copy, summary text, or count information.
Type ReactNode
classNameMerged onto the caption paragraph.
Type string
childrenEmpty-state copy or a richer no-data message.
Type ReactNode
colSpanOverrides the automatically derived column span when the empty row should cover a different number of columns.
Type number
Motion tr propsYou can still override animate, initial, transition, or className when customizing the empty state row.
Type ComponentPropsWithoutRef<typeof motion.tr>
rowsNumber of placeholder rows to render.
Type number·Default 3
TableRow propsOptional TableRow props such as index or className passed to each loading row.
Type Omit<TableRowProps, "children" | "variant">
activeStrengthens the visual treatment and enables the active sort direction state for the parent column header.
Type boolean·Default false
directionRotates the chevron when the current active sort direction is descending.
Type "asc" | "desc"·Default asc
alignKeeps the sort button aligned with the header cell it lives in, including full-width right-aligned targets.
Type "left" | "center" | "right"·Default left
childrenVisible sort label.
Type ReactNode
aria-labelOptional override for the generated sort label announced to screen readers.
Type string
classNameMerged onto the button wrapper.
Type string
alignAligns the page summary and previous/next controls as a group below the table.
Type "left" | "center" | "right"·Default right
pageCurrent one-based page index.
Type number
pageCountTotal number of available pages.
Type number
onPageChangeCalled when the previous or next control changes pages.
Type (page: number) => void
pageSizeUsed with totalItems to render a range label such as 1–5 of 12.
Type number
totalItemsTotal item count shown in the optional range label.
Type number
showPageInfoToggles the range or page summary text.
Type boolean·Default true
Install the exact registry entry shown on the right when you want the component file and its declared runtime dependencies together.
Dependencies: motion, lucide-react.
This page lives in the Components section, but the install itself is the shared Iconiq table primitive rather than a Radix or Base UI table wrapper.
The provider switch is shown for section consistency, but both Radix UI and Base UI options are disabled because there is only one table install on this page.
The generated registry file is /r/table.json.
Contact
Additionally, if you find any bug or issue, feel free to raise an issue.
| Ada Lovelace | Engineer | Active | $4,200 |
| Alan Turing | Researcher | Active | $5,800 |
| Grace Hopper | Architect | Pending | $3,100 |
| Katherine Johnson | Analyst | Active | $5,100 |
| Linus Torvalds | Maintainer | Archived | $2,750 |
| Margaret Hamilton | Lead | Active | $6,400 |
| Radia Perlman | Engineer | Active | $4,700 |
| Tim Berners-Lee | Architect | Pending | $3,900 |