Components
Table A generic data table with clickable sortable headers for local data, or remote mode that delegates sorting to your database.
CLI Manual
$ npx litefy@latest add table
$ pnpm dlx litefy@latest add table
$ yarn dlx litefy@latest add table
$ bun --bun litefy@latest add table
Table is data-driven: pass data plus a columns config; sortable: true makes a column header click-to-sort.
Task Owner Priority Points Done
Design review Ada high 3 ✓ API integration Linus medium 8 — Write tests Grace high 5 — Release notes Ada low 1 ✓
View Code "use client";
import { Table } from "@/ui";
interface Task {
title: string;
owner: string;
priority: "high" | "medium" | "low";
points: number;
done: boolean;
}
const tasks: Task[] = [
{ title: "Design review", owner: "Ada", priority: "high", points: 3, done: true },
{ title: "API integration", owner: "Linus", priority: "medium", points: 8, done: false },
{ title: "Write tests", owner: "Grace", priority: "high", points: 5, done: false },
{ title: "Release notes", owner: "Ada", priority: "low", points: 1, done: true },
];
export default function Demo() {
return (
<div className="w-full max-w-xl">
<Table
data={tasks}
getKey={(row) => row.title}
columns={[
{ key: "title", header: "Task", sortable: true },
{ key: "owner", header: "Owner", width: "7rem" },
{
key: "priority",
header: "Priority",
sortable: true,
width: "7rem",
render: (row) => (
<span
className={
row.priority === "high"
? "font-medium text-danger"
: row.priority === "medium"
? "text-warning"
: "text-muted-foreground"
}
>
{row.priority}
</span>
),
},
{ key: "points", header: "Points", sortable: true, width: "6rem" },
{ key: "done", header: "Done", width: "5rem", render: (row) => (row.done ? "✓" : "—") },
]}
/>
</div>
);
}
Pass controlled sort + onSortChange to enter remote mode — sorting is delegated to your database; pair with useRemoteSort for state and query building.
Mechanical keyboard Peripherals $129 42 Wireless mouse Peripherals $49 120 4K monitor Displays $349 18 USB-C hub Accessories $39 200 Laptop stand Accessories $29 75
Remote mode: sorting is delegated to the database via (unsorted)
View Code "use client";
import * as React from "react";
import { Table, useRemoteSort } from "@/ui";
interface Product {
name: string;
category: string;
price: number;
stock: number;
}
const allProducts: Product[] = [
{ name: "Mechanical keyboard", category: "Peripherals", price: 129, stock: 42 },
{ name: "Wireless mouse", category: "Peripherals", price: 49, stock: 120 },
{ name: "4K monitor", category: "Displays", price: 349, stock: 18 },
{ name: "USB-C hub", category: "Accessories", price: 39, stock: 200 },
{ name: "Laptop stand", category: "Accessories", price: 29, stock: 75 },
];
export default function Demo() {
const remote = useRemoteSort<keyof Product>({
sortableKeys: ["name", "price", "stock"],
onSortChange: (sort) => {
// In production: refetch with ?sortKey=...&sortOrder=...
console.log("query:", `?sortKey=${sort.key}&sortOrder=${sort.direction}`);
},
});
const sorted = React.useMemo(() => {
const active = remote.sort;
if (!active) return allProducts;
const { key, direction } = active;
return [...allProducts].sort((a, b) => {
const cmp = a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0;
return direction === "asc" ? cmp : -cmp;
});
}, [remote.sort]);
return (
<div className="flex w-full max-w-xl flex-col gap-2">
<Table
data={sorted}
sort={remote.sort}
onSortChange={remote.setSort}
columns={[
{
key: "name",
header: "Name",
sortable: true,
render: (row) => <span className="font-medium">{row.name}</span>,
},
{ key: "category", header: "Category" },
{ key: "price", header: "Price", sortable: true, width: "7rem", render: (row) => `$${row.price}` },
{ key: "stock", header: "Stock", sortable: true, width: "6rem" },
]}
/>
<p className="text-xs text-muted-foreground">
Remote mode: sorting is delegated to the database via{" "}
<code className="rounded bg-muted px-1">{remote.queryString || "(unsorted)"}</code>
</p>
</div>
);
}
The header stays fixed while the body scrolls in its own container — override the built-in body height via classNames.body.
Design review Ada high API integration Linus medium Write tests Grace low Release notes Edsger high Fix flaky test Ada medium Update docs Linus low Refactor hooks Grace high Optimize bundle Edsger medium Add telemetry Ada low Rotate keys Linus high Patch CVE Grace medium Migrate config Edsger low
View Code "use client"; import { Table } from "@/ui"; interface Task { title: string; owner: string; priority: "high" | "medium" | "low"; done: boolean; } const titles = [ "Design review", "API integration", "Write tests", "Release notes", "Fix flaky test", "Update docs", "Refactor hooks", "Optimize bundle", "Add telemetry", "Rotate keys", "Patch CVE", "Migrate config", ]; const owners = ["Ada", "Linus", "Grace", "Edsger"]; const priorities = ["high", "medium", "low"] as const; const tasks: Task[] = titles.map((title, i) => ({ title, owner: owners[i % owners.length], priority: priorities[i % priorities.length], done: i % 3 === 0, })); export default function Demo() { return ( <div className="w-full max-w-xl"> <Table data={tasks} getKey={(row) => row.title} classNames={{ body: "h-48" }} columns={[ { key: "title", header: "Task", sortable: true }, { key: "owner", header: "Owner", sortable: true }, { key: "priority", header: "Priority" }, ]} /> </div> ); }
Table is assembled from four atoms — custom tables reuse the same frame, keeping the <colgroup> identical in both tables.
Prop Type Default Description dataT[]- Row data. Required columnsTableColumn<T>[]- Column config. Required getKey(row: T, index: number) => React.Keyindex Row key extractor sortSortState<T> | null- Controlled sort state; enables remote mode onSortChange(sort: SortState<T>) => void- Fired on header clicks (both modes) initialSortDirection"asc" | "desc""asc"Direction used on the first click of a column emptyReact.ReactNode"No data"Rendered when data is empty classNameClassNameValue- Custom classes, applied to the outer frame classNameswrapper / table / head / headRow / headCell / body / row / cell- Custom classes per part (head / body target the header strip and the body scroller) styles{ wrapper?, table? }- Inline styles for the wrapper and both tables
Field Type Default Description keykeyof T & string- Row field to display. Required headerReact.ReactNodekeyHeader label render(row: T) => React.ReactNoderaw value Custom cell renderer sortablebooleanfalseMake the header click-to-sort compare(a: T, b: T) => number< / >Custom comparator used by local sorting align"left" | "right" | "center""left"Cell alignment; the design spec keeps columns start-aligned (center is fine for control columns) widthstring- CSS width for the column ("8rem", "25%", "20ch"), applied to the <col>; width-less columns share the leftover equally classNameClassNameValue- Custom classes applied to the column's header cell and body cells (e.g. max-w-* for long-text columns)
Part Element Built-in classes Description TableRoot<div>w-full overflow-hidden rounded-lg borderOuter frame TableHead<div>overflow-hiddenHeader strip; never scrolls TableBody<div>h-96 overflow-y-autoBody scroller TableBase<table>w-full table-fixed border-collapse text-smShared table element
All parts accept native element props plus className.
{ key: keyof T & string; direction: "asc" | "desc" } | null