Sortable table
FreeA table with sortable columns, optional selection, loading and empty states.
pnpm dlx shadcn@latest add https://caliperui.site/r/sortable-table.json| Atlas platform | Mara Chen | Active | $48,000 | |
|---|---|---|---|---|
| Harbour redesign | Noor Haddad | In review | $21,000 | |
| Release notes | Ivo Petrov | Active | $6,500 | |
| Wayfinder | Sana Okafor | Paused | $33,000 |
Sorted by Project, ascending.
When to use #
Use it for
- Records people need to sort and select.
- Admin screens with bulk actions.
Skip it when
- Thousands of rows; use a virtual table.
Installation #
pnpm dlx shadcn@latest add https://caliperui.site/r/sortable-table.json
Adds its 4 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above. It is built from SortableTable and sortRows.
import { useState } from "react" import { SortableTable } from "@/components/ui/sortable-table" const projects = [ { id: "atlas", name: "Atlas platform", owner: "Mara Chen", status: "Active", budget: 48000 }, { id: "harbour", name: "Harbour redesign", owner: "Noor Haddad", status: "In review", budget: 21000 }, { id: "notes", name: "Release notes", owner: "Ivo Petrov", status: "Active", budget: 6500 }, { id: "wayfinder", name: "Wayfinder", owner: "Sana Okafor", status: "Paused", budget: 33000 }, ]; export function ProjectTable() { const [selection, setSelection] = useState<string[]>([]); return ( <div className="w-full max-w-2xl"> <SortableTable caption="Projects" rows={projects} getRowId={(row) => row.id} selection={selection} onSelectionChange={setSelection} defaultSort={{ id: "name", direction: "asc" }} columns={[ { id: "name", header: "Project", cell: (row) => row.name, sortValue: (row) => row.name }, { id: "owner", header: "Owner", cell: (row) => row.owner, sortValue: (row) => row.owner }, { id: "status", header: "Status", cell: (row) => row.status, sortValue: (row) => row.status }, { id: "budget", header: "Budget", cell: (row) => `$${row.budget.toLocaleString("en-US")}`, sortValue: (row) => row.budget, align: "end" }, ]} /> </div> ); }
API reference #
SortableTable
component
sortRows
component
No props of its own; it accepts the native props of its root element.
Accessibility #
It exposes status semantics, with aria-busy, aria-live, aria-label, aria-sort and aria-hidden kept in step with state. Changes that matter are announced through a live region.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.