Components
Selectable Table
A table with a built-in checkbox column and select-all with indeterminate state. Pagination is composed externally with Pagination and usePagination.
Installation
- $npx litefy@latest add selectable-table
- $pnpm dlx litefy@latest add selectable-table
- $yarn dlx litefy@latest add selectable-table
- $bun --bun litefy@latest add selectable-table
Usage
SelectableTable wraps Table with a built-in checkbox column — select-all affects only the current page and preserves other pages' selections.
| Task | Owner | Priority |
|---|
| Design review | Ada | high | |
| API integration | Linus | medium | |
| Write tests | Grace | high | |
| Release notes | Ada | low | |
| Fix flaky test | Linus | high | |
| Update docs | Grace | medium | |
| Refactor hooks | Ada | low | |
| Optimize bundle | Linus | high | |
| Add telemetry | Grace | medium | |
| Rotate keys | Ada | high | |
| Patch CVE | Linus | low | |
| Migrate config | Grace | medium |
0 selected
"use client";import * as React from "react";import { SelectableTable } from "@/ui";interface Task { id: string; title: string; owner: string; priority: "high" | "medium" | "low";}const tasks: Task[] = [ { id: "t-01", title: "Design review", owner: "Ada", priority: "high" }, { id: "t-02", title: "API integration", owner: "Linus", priority: "medium" }, { id: "t-03", title: "Write tests", owner: "Grace", priority: "high" }, { id: "t-04", title: "Release notes", owner: "Ada", priority: "low" }, { id: "t-05", title: "Fix flaky test", owner: "Linus", priority: "high" }, { id: "t-06", title: "Update docs", owner: "Grace", priority: "medium" }, { id: "t-07", title: "Refactor hooks", owner: "Ada", priority: "low" }, { id: "t-08", title: "Optimize bundle", owner: "Linus", priority: "high" }, { id: "t-09", title: "Add telemetry", owner: "Grace", priority: "medium" }, { id: "t-10", title: "Rotate keys", owner: "Ada", priority: "high" }, { id: "t-11", title: "Patch CVE", owner: "Linus", priority: "low" }, { id: "t-12", title: "Migrate config", owner: "Grace", priority: "medium" },];export default function Demo() { const [selected, setSelected] = React.useState<string[]>([]); return ( <div className="w-full max-w-xl"> <SelectableTable data={tasks} rowKey={(row) => row.id} selected={selected} onSelectionChange={setSelected} columns={[ { key: "title", header: "Task" }, { key: "owner", header: "Owner" }, { key: "priority", header: "Priority", render: (row) => ( <span className={ row.priority === "high" ? "font-medium text-danger" : row.priority === "medium" ? "text-warning" : "text-muted-foreground" } > {row.priority} </span> ), }, ]} /> <p className="mt-2 text-sm text-muted-foreground">{selected.length} selected</p> </div> );}Composition with Pagination
The table renders whatever data holds — let usePagination own the page state and feed Pagination externally.
| ID | Name | Village |
|---|
| r-01 | Ada | Eastwood | |
| r-02 | Linus | Riverside | |
| r-03 | Grace | Hillcrest | |
| r-04 | Edsger | Lakeside | |
| r-05 | Hopper | Eastwood |
"use client";import * as React from "react";import { Button, Pagination, SelectableTable, usePagination } from "@/ui";import { Download } from "lucide-react";interface Resident { id: string; name: string; village: string;}const TOTAL = 23;const PAGE_SIZE = 5;const names = ["Ada", "Linus", "Grace", "Edsger", "Hopper", "Dijkstra"];const villages = ["Eastwood", "Riverside", "Hillcrest", "Lakeside"];const residents: Resident[] = Array.from({ length: TOTAL }, (_, i) => ({ id: `r-${String(i + 1).padStart(2, "0")}`, name: names[i % names.length], village: villages[i % villages.length],}));const totalPages = Math.ceil(TOTAL / PAGE_SIZE);export default function Demo() { const pagination = usePagination({ base: 1, total: totalPages }); const [data, setData] = React.useState(() => residents.slice(0, PAGE_SIZE)); const [loading, setLoading] = React.useState(false); const skipFirst = React.useRef(true); React.useEffect(() => { if (skipFirst.current) { skipFirst.current = false; return; } setLoading(true); const timer = window.setTimeout(() => { setData(residents.slice((pagination.index - 1) * PAGE_SIZE, pagination.index * PAGE_SIZE)); setLoading(false); }, 400); return () => window.clearTimeout(timer); }, [pagination.index]); const [selected, setSelected] = React.useState<string[]>([]); return ( <section className="flex w-full max-w-xl flex-col gap-2"> <SelectableTable data={data} rowKey={(row) => row.id} selected={selected} onSelectionChange={setSelected} loading={loading} columns={[ { key: "id", header: "ID" }, { key: "name", header: "Name" }, { key: "village", header: "Village" }, ]} /> <footer className="flex items-center justify-between gap-2 text-sm text-muted-foreground"> <span className="min-w-0 truncate">{selected.length} selected · {TOTAL} total</span> <div className="flex shrink-0 items-center gap-1"> <Button variant="text" aria-label="Export selected" disabled={selected.length === 0}> <Download /> </Button> <Pagination page={pagination.index} totalPages={totalPages} onPageChange={pagination.goTo} disabled={loading} /> </div> </footer> </section> );}Large datasets
Pair the TableBody scroller with useVirtualScroll for row counts too large to render at once.
The selection column lives under the internal key __selection — avoid a data field with that name.
API Reference
SelectableTable<T>
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | - | Row data (the current page). Required |
columns | TableColumn<T>[] | - | Column config, the checkbox column is prepended automatically. Required |
rowKey | (row: T) => string | - | Row key extractor and selection unit. Required |
selected | string[] | - | Controlled selection (row keys) |
defaultSelected | string[] | [] | Initial selection for uncontrolled usage |
onSelectionChange | (selected: string[]) => void | - | Fired on every row / select-all toggle |
loading | boolean | false | Default empty text becomes "Loading..." |
empty | React.ReactNode | "No data" | Rendered when data is empty |
className | ClassNameValue | - | Custom classes, forwarded to the inner Table |
classNames | Table slots | - | Custom classes per part |
styles | { wrapper?, table? } | - | Forwarded to the inner Table |
All remaining Table props (sort / onSortChange, getKey, …) are inherited; getKey is derived from rowKey.