Components
Data table
Table with sortable headers (aria-sort, ink glyphs), a column filter, a columns popover and paging, on TanStack Table and the Eloquent table.
Preview
| Budget passes second reading | Politics | 1,240 |
| Port strike enters third day | Business | 980 |
| Museum reopens its east wing | Culture | 760 |
| Telescope finds a quiet star | Science | 1,105 |
| Council rejects the tram plan | Politics | 870 |
Install
bash
npx shadcn add @eloquent/data-tableThis adds @tanstack/react-table (v9; Base UI has no data table) and the table, button, input, popover and checkbox items.
Usage
tsx
import { createDataTableColumnHelper, DataTable } from "@/components/ui/data-table";
type Story = { title: string; desk: string; words: number };
const helper = createDataTableColumnHelper<Story>();
// Keep columns and data stable: module scope or useMemo. `helper.columns([...])` types mixed value columns.
const columns = helper.columns([
helper.accessor("title", { id: "title", header: "Story" }),
helper.accessor("desk", { id: "desk", header: "Desk" }),
helper.accessor("words", { id: "words", header: "Words" }),
]);
<DataTable
columns={columns}
data={stories}
caption="Stories filed this week."
labels={{
filter: "Filter stories",
columns: "Columns",
previous: "Previous",
next: "Next",
page: (page, count) => `Page ${page} of ${count}`,
empty: "No stories match.",
sorted: (column, direction) => `Sorted by ${column}, ${direction === "asc" ? "ascending" : "descending"}`,
}}
pageSize={5}
numericColumns={["words"]}
filterColumn="title"
/>Use a string header: it names the sort button and the checkbox in the columns popup. The component carries no English text; every string is a prop. Pass locale so text sorts by the language (Vietnamese diacritics included).
States
| Words | |
|---|---|
| No stories match. | |
| Words | |
|---|---|
| Budget passes second reading | 1240 |
| Port strike enters third day | 980 |
| Museum reopens its east wing | 760 |
| State | Treatment |
|---|---|
| Sortable header | Kicker button with ▲▼ stacked in grey |
| Sorted | One ink glyph (▲ ascending, ▼ descending) and aria-sort on the header |
| Sort button | Ink fill on press; 2px ink outline on focus; no hover |
| Empty | One ruled row with a message |
| Pager at an end | The disabled button: grey text and rule, not allowed |
Keyboard
| Key | Action |
|---|---|
| Tab | Moves through the filter, the columns button, the table's scroll region (with scrollLabel), the sort buttons and the pager |
| Enter / Space on a header | Sorts ascending, then descending, then clears; the change is announced |
| Enter / Space on the columns button | Opens the columns popup |
| Space on a column checkbox | Shows or hides the column |
| Esc | Closes the popup and returns focus to the columns button |
| Enter / Space on Previous or Next | Changes the page |
Vietnamese
| Ngân sách qua vòng đọc thứ hai | Chính trị | 1.240 |
| Đình công cảng kéo sang ngày thứ ba | Kinh doanh | 980 |
| Bảo tàng mở lại cánh phía đông | Văn hoá | 760 |
| Kính thiên văn tìm thấy một ngôi sao lặng | Khoa học | 1.105 |
| Hội đồng bác kế hoạch xe điện | Chính trị | 870 |
Ink adaptation
Upstream's recipe uses a dropdown menu to hide columns and ghost sort buttons with chevrons. Here the column toggle is a popover with checkboxes, paging is two plain buttons, and sort state is an ink ▲ or ▼ with aria-sort, so it never rests on colour. Row hover fills are removed.