Data table
Records you narrow with a sentence, sort by pressing a heading (the column turns ink and the rows glide), and page at a folio, or read on as the table ends in the rest of itself.
Report an issue with Data tableExample
projects
| Harbour Line | web | 2026 | £48,000 |
| Salt & Rye | identity | 2026 | £22,000 |
| Common Ground | web | 2026 | £61,000 |
| Quiet Hours | motion | 2026 | £42,000 |
| Glasshouse | identity | 2026 | £27,500 |
| Blue Hour | web | 2026 | £54,000 |
| Almanac | identity | 2026 | £30,000 |
| Longshore | motion | 2026 | £36,000 |
1 to 8 of 24 projects
| Common Ground | web | 2026 | £61,000 |
| Blue Hour | web | 2026 | £54,000 |
| Harbour Line | web | 2026 | £48,000 |
| Quiet Hours | motion | 2026 | £42,000 |
| Signal Box | web | 2025 | £40,500 |
5 of 24 projects
A filter that keeps nothing
projects
| No print projects. | |||
0 projects
The last page
| Undertow | motion | 2025 | £23,500 |
| Blue Hour | web | 2026 | £54,000 |
| Marram | identity | 2023 | £11,000 |
| Signal Box | web | 2025 | £40,500 |
| Cinder | motion | 2024 | £19,000 |
| Almanac | identity | 2026 | £30,000 |
| Fen | web | 2023 | £21,000 |
| Longshore | motion | 2026 | £36,000 |
17 to 24 of 24 projects
Tail, all shown
| Harbour Line | web | 2026 | £48,000 |
| Salt & Rye | identity | 2026 | £22,000 |
| Nightjar | motion | 2025 | £31,000 |
| Kestrel Press | identity | 2025 | £18,500 |
| Low Tide | web | 2024 | £26,000 |
| Common Ground | web | 2026 | £61,000 |
| Paper Moon | motion | 2024 | £14,000 |
7 projects
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, button, pagination, select, and table along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/data-table.jsonUsage
"use client"import { DataTable, type DataTableColumn, type DataTableFilter } from "@/components/ui/data-table"type Project = { name: string; kind: "web" | "identity" | "motion"; year: number; fee: number }// Fictional studio work.const projects: Project[] = [["Harbour Line", "web", 2026, 48000], ["Salt & Rye", "identity", 2026, 22000], ["Nightjar", "motion", 2025, 31000],["Kestrel Press", "identity", 2025, 18500], ["Low Tide", "web", 2024, 26000], ["Common Ground", "web", 2026, 61000],["Paper Moon", "motion", 2024, 14000], ["Field Notes", "identity", 2023, 9500], ["Orchard", "web", 2025, 37500],["Quiet Hours", "motion", 2026, 42000], ["Tin Roof", "identity", 2024, 12000], ["Northlight", "web", 2023, 29000],["Glasshouse", "identity", 2026, 27500], ["Slow Burn", "motion", 2023, 8000], ["Wayfarer", "web", 2024, 33000],["Linden", "identity", 2025, 16000], ["Undertow", "motion", 2025, 23500], ["Blue Hour", "web", 2026, 54000],["Marram", "identity", 2023, 11000], ["Signal Box", "web", 2025, 40500], ["Cinder", "motion", 2024, 19000],["Almanac", "identity", 2026, 30000], ["Fen", "web", 2023, 21000], ["Longshore", "motion", 2026, 36000],].map(([name, kind, year, fee]) => ({ name, kind, year, fee }) as Project)const pounds = new Intl.NumberFormat("en-GB", { style: "currency", currency: "GBP", maximumFractionDigits: 0 })const columns: DataTableColumn<Project>[] = [{ id: "name", header: "Project", value: (p) => p.name, primary: true },{ id: "kind", header: "Kind", value: (p) => p.kind },{ id: "year", header: "Year", value: (p) => p.year, numeric: true },{ id: "fee", header: "Fee", value: (p) => p.fee, cell: (p) => pounds.format(p.fee), numeric: true },]const filters: DataTableFilter<Project>[] = (["web", "identity", "motion"] as const).map((k) => ({ id: k, label: k, test: (p) => p.kind === k }))const byName = (p: Project) => p.nameexport default function Example() {return (<div className="grid w-full gap-16"><DataTable data={projects} columns={columns} filters={filters} noun="projects" caption="The studio's projects" getRowId={byName} defaultSort={{ id: "year", dir: -1 }} /><DataTable data={projects} columns={columns} noun="projects" caption="The studio's projects, by fee" getRowId={byName} defaultSort={{ id: "fee", dir: -1 }} variant="tail" pageSize={5} tableVariant="forte" /></div>)}Contract
- Underneath:
table,paginationandselect, composed; native controls throughout. - Anatomy:
<div class="ot-data-table" data-variant="folio | tail">holding.ot-data-table-filter(a sentence, "Show [all] projects", its chosen word aot-select), theot-table, and.ot-data-table-foot:.ot-data-table-count(role="status", "1 to 8 of 24 projects") and either the folio pager or, in a tail,.ot-data-table-more. Rows that come into view carrydata-arrivingand--k, their place in the turn. - The filter is a sentence, not a toolbar: the chosen word is italic, as yours, and the noun ends it. Narrowing returns to the first page and the rows that stay glide to their places, as the table's do on a re-sort; the sorted column is in ink (forte sets it loud). Pressing a heading sorts by it, a numeric column largest first; pressing again turns the order round.
variant: folio (the default): a page at a time; the count stands at the start of the foot line in pencil, the book's folio (the page over the total,pagination's folio) at its end. Turning a page keeps focus in the pager, and the new page's rows arrive in turn. tail: the table ends in the rest of itself, as the collapsible does: "and 16 more" is the control, straight under the last rule; the next rows arrive in turn under it, and once all are shown it says Show fewer.- Empty: a filter that keeps nothing says so in a line ("No print projects.") and offers "Show all projects"; there is no picture.
- Keyboard: native buttons, links and the select. Reduced motion: the tempos fall to nothing, so rows simply appear.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
data / columns | T[] / DataTableColumn<T>[] | The records, and the columns: id, header, value (what it sorts by and shows), and optionally cell (how it shows), numeric (right-aligned figures, largest first on the first press), primary (the row's name) and sortable (false to leave a heading unpressable). | |
caption | string | Names the table for screen readers. | |
noun | string | "rows" | What the rows are, in the plural. It ends the filter's sentence (Show all projects) and the count (1 to 8 of 24 projects). |
filters | { id; label; test }[] | Ways to narrow the rows, offered as the chosen word of a sentence: Show [identity] projects. The label is lower case to sit in the sentence. all always comes first. Narrowing goes back to the first page, and the rows that stay glide to their new places. | |
filter / defaultFilter / onFilterChange | string | "all" | The filter, controlled or not. |
defaultSort | { id: string; dir: 1 | -1 } | The column the rows start sorted by. Pressing a heading sorts by it; pressing again turns the order round. | |
variant | "folio" | "tail" | "folio" | folio: a page at a time, the book's folio (the page over the total) at the end of the foot line, the count at its start. tail: the table ends in the rest of itself, as a collapsible does; "and 16 more" is the control, and the next rows arrive in turn under the last; once all are shown it says Show fewer. |
page / defaultPage / onPageChange | number | 1 | The page, controlled or not. In a tail, how many pages are shown. |
pageSize | number | 8 | Rows to a page. |
pageHref | (page: number) => string | ?page=n | Where a page link goes when opened in a new tab. A plain press turns the page in place and keeps focus in the pager. |
getRowId | (row, index) => string | the index | A stable name for each row, so it keeps its place and glides through sorting and filtering. |
tableVariant | "ink" | "forte" | "ink" | The table's own setting. forte sets the sorted column loud, and the loudness passes as you re-sort. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Data table | "Renaissance." two colours; the book's folio | The filter a sentence, the sorted column in ink, the count and the folio on the foot line |
| Tail data table | The collapsible's tail; "the silence that heals" captions | The table ends in the rest of itself: "and 16 more" |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Data table | Glide, then arrive | Narrowed or sorted, the rows glide; a new page's rows arrive in turn |
| Tail data table | Arrive | The next rows arrive in turn under the last |