caliper uidocs

Data table

Free

Accessible table markup with consistent cell styling and a scrollable region.

Installs components/ui/data-table.tsxDepends on lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/data-table.json
Invoices due this month
InvoiceCustomerStatus
INV-0417Halden Analytics4,280.002 OctPaid
INV-0415Lumen Labs918.005 OctFailed
INV-0416Ostra Freight12,960.5014 OctDue 14 Oct
INV-0414Northwind Studio7,125.0018 OctDue 18 Oct
INV-0418Kestrel Bio2,640.0021 OctDraft

Click a row, then use ↑ ↓ to move and x to select.

When to use #

Use it for

  • Static tabular data that needs correct semantics.
  • A base to build custom tables on.

Skip it when

  • Sorting or selection is needed; use the sortable table.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/data-table.json

Adds its 2 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 DataTable, DataTableCell, DataTableHead, DataTableRow and DataTableBulkBar.

example.tsx
import { useState } from "react"
import { DataTable, DataTableCell, DataTableHead, DataTableRow, DataTableBulkBar } from "@/components/ui/data-table"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"

export function InvoiceTable() {
  const data = [
    { id: "INV-0418", customer: "Kestrel Bio", amount: 2640, due: "2026-10-21", status: "Draft", tone: "neutral" as const },
    { id: "INV-0417", customer: "Halden Analytics", amount: 4280, due: "2026-10-02", status: "Paid", tone: "success" as const },
    { id: "INV-0416", customer: "Ostra Freight", amount: 12960.5, due: "2026-10-14", status: "Due 14 Oct", tone: "warning" as const },
    { id: "INV-0415", customer: "Lumen Labs", amount: 918, due: "2026-10-05", status: "Failed", tone: "error" as const },
    { id: "INV-0414", customer: "Northwind Studio", amount: 7125, due: "2026-10-18", status: "Due 18 Oct", tone: "warning" as const },
  ];
  const [sort, setSort] = useState<{ key: "amount" | "due"; dir: 1 | -1 }>({ key: "due", dir: 1 });
  const [selected, setSelected] = useState<string[]>([]);
  const [cursor, setCursor] = useState(0);
  const [done, setDone] = useState("");
  const rows = [...data].sort((a, b) => (sort.key === "amount" ? a.amount - b.amount : a.due.localeCompare(b.due)) * sort.dir);
  const money = (value: number) => value.toLocaleString("en-US", { minimumFractionDigits: 2 });
  const total = rows.filter((row) => selected.includes(row.id)).reduce((sum, row) => sum + row.amount, 0);
  const toggle = (id: string) => setSelected((current) => (current.includes(id) ? current.filter((item) => item !== id) : [...current, id]));
  const flip = (key: "amount" | "due") => setSort((current) => ({ key, dir: current.key === key ? (current.dir === 1 ? -1 : 1) : key === "amount" ? -1 : 1 }));
  const state = (key: "amount" | "due") => (sort.key === key ? (sort.dir === 1 ? "ascending" : "descending") : "none");
  return (
    <div
      className="w-full max-w-2xl"
      onKeyDown={(event) => {
        if (event.key === "ArrowDown" || event.key === "j") setCursor((value) => Math.min(rows.length - 1, value + 1));
        else if (event.key === "ArrowUp" || event.key === "k") setCursor((value) => Math.max(0, value - 1));
        else if (event.key === "x") toggle(rows[cursor].id);
        else if (event.key === "Escape") setSelected([]);
        else return;
        event.preventDefault();
      }}
    >
      <DataTable caption="Invoices due this month">
        <thead>
          <tr>
            <DataTableHead className="w-10"><Checkbox aria-label="Select all invoices" checked={selected.length === rows.length} indeterminate={selected.length > 0 && selected.length < rows.length} onChange={(event) => setSelected(event.currentTarget.checked ? rows.map((row) => row.id) : [])} /></DataTableHead>
            <DataTableHead>Invoice</DataTableHead>
            <DataTableHead>Customer</DataTableHead>
            <DataTableHead numeric sort={state("amount")} onSort={() => flip("amount")}>Amount, USD</DataTableHead>
            <DataTableHead sort={state("due")} onSort={() => flip("due")}>Due</DataTableHead>
            <DataTableHead>Status</DataTableHead>
          </tr>
        </thead>
        <tbody>
          {rows.map((row, index) => (
            <DataTableRow key={row.id} selected={selected.includes(row.id)} cursor={index === cursor} onClick={() => setCursor(index)}>
              <DataTableCell><Checkbox aria-label={`Select ${row.id}`} checked={selected.includes(row.id)} onChange={() => toggle(row.id)} /></DataTableCell>
              <DataTableCell className="font-mono text-[13px]">{row.id}</DataTableCell>
              <DataTableCell>{row.customer}</DataTableCell>
              <DataTableCell numeric className="font-medium">{money(row.amount)}</DataTableCell>
              <DataTableCell className="tabular-nums text-muted-foreground">{new Date(row.due).toLocaleDateString("en-GB", { day: "numeric", month: "short" })}</DataTableCell>
              <DataTableCell><Badge variant={row.tone}>{row.status}</Badge></DataTableCell>
            </DataTableRow>
          ))}
        </tbody>
      </DataTable>
      <DataTableBulkBar count={selected.length} summary={`$${money(total)}`}>
        <Button size="sm" onClick={() => { setDone(`Reminders sent for ${selected.length} invoice${selected.length === 1 ? "" : "s"}.`); setSelected([]); }}>Send reminder</Button>
        <Button size="sm">Export</Button>
      </DataTableBulkBar>
      <p role="status" className="mt-2 text-sm text-muted-foreground">{done || "Click a row, then use ↑ ↓ to move and x to select."}</p>
    </div>
  );
}

API reference #

DataTable

component

Also accepts the native props of its root element and a ref.

PropTypeDefault
caption *string—

DataTableCell

component

Also accepts the native props of its root element and a ref.

PropTypeDefault
numericbooleanfalse

DataTableHead

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
numericbooleanfalseRight-aligned with tabular figures, for money and counts.
sort"none" | "ascending" | "descending"—Makes the header a sort button.
onSort(() => void)—

DataTableRow

component

Also accepts the native props of its root element and a ref.

PropTypeDefault
selectedboolean—
cursorboolean—

DataTableBulkBar

component
PropTypeDefault
count *number—
summaryReactNode—
children *ReactNode—
classNamestring—

Accessibility #

It exposes region and toolbar semantics, with aria-label, aria-sort, aria-hidden, aria-selected and aria-live 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 declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.