caliper uidocs

Sortable table

Free

A table with sortable columns, optional selection, loading and empty states.

Installs components/ui/sortable-table.tsxDepends on lucide-reactMotion honours reduced motion
pnpm dlx shadcn@latest add https://caliperui.site/r/sortable-table.json
Select rows to act on them
Projects
Atlas platformMara ChenActive$48,000
Harbour redesignNoor HaddadIn review$21,000
Release notesIvo PetrovActive$6,500
WayfinderSana OkaforPaused$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 #

terminal
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.

example.tsx
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
PropTypeDefaultDescription
caption *string—
columns *readonly TableColumn<T>[]—
rows *readonly T[]—
getRowId *(row: T) => string—
sortSortState—
defaultSortSortStatenull
onSortChange((sort: SortState) => void)—
selectionreadonly string[]—Selected row ids. Provide with `onSelectionChange` to show checkboxes.
onSelectionChange((ids: string[]) => void)—
selectionActionsReactNode—
loadingboolean—
emptyLabelstring"No results."
classNamestring—

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.