DataTable
Anatomy
+----------------------------------------------------+| caption (row count + active sort, screen-reader) || +------------------------------------------------+ || | [_] Name ↕ Email ↕ Joined ↓ Spend ↕ | | ← header row + sort| | [_] filter… filter… filter… filter… | | ← optional text filters| +------------------------------------------------+ || | [x] Alice alice@… 2026-05-25 $1,240 | | ← rows| | [ ] Bob bob@… 2026-04-30 $812 | || | [_] … | || +------------------------------------------------+ || Page 1 of 5 « ‹ › » | ← optional pagination+----------------------------------------------------+When to use
- Tabular lists you sort, filter, or select against.
- Admin / dashboard surfaces.
- Anywhere
<Table>(the unstyled primitive) was almost enough but you ended up wiring sort/filter/select state by hand.
When to reach for <Table> instead
- Display-only data with no interaction.
- A list that wants total control over markup (
thead/tbody/trexposed without the helixui wrapper).
Examples
Minimal read-only
import { DataTable, type ColumnDef } from '@helixui/core';
interface User { id: string; name: string; email: string; spend: number; }
const columns: ColumnDef<User>[] = [ { id: 'name', header: 'Name', accessor: (u) => u.name, sortable: true, filter: 'text' }, { id: 'email', header: 'Email', accessor: (u) => u.email, sortable: true }, { id: 'spend', header: 'Spend', accessor: (u) => u.spend, sortable: true, align: 'right', cell: (v) => `$${(v as number).toLocaleString()}` },];
<DataTable data={users} columns={columns} />With selection
const [selected, setSelected] = useState<Set<string>>(new Set());
<DataTable data={users} columns={columns} selectable onSelectionChange={setSelected}/>Paginated + filterable
<DataTable data={users} columns={columns} pageSize={25} emptyState={<EmptyState title="No users yet" />}/>Server-driven sort
const [sort, setSort] = useState<{ id: string; direction: 'asc' | 'desc' } | null>( { id: 'name', direction: 'asc' },);
<DataTable data={data} // already sorted by the server columns={columns} sort={sort} onSortChange={(next) => { setSort(next); fetchPage({ sort: next }); }}/>Performance notes
- The default render mode is all rows. For ≤ 2,000 rows this is fine; React’s reconciler handles it without dropping frames.
- For larger sets, set
pageSize. Virtualization is on the roadmap (see RFC 0001) but not yet shipped. - Sorting and filtering use
Array.prototype.sortandfilter— they’re synchronous. For tables larger than ~10k rows, sort/filter on the server.
Roadmap
Planned in subsequent releases:
- Virtualization for tens of thousands of rows.
- Column resizing.
- Column visibility toggle.
- Sticky / pinned columns.
- Inline editing.
- Tree / grouped rows.
If you need any of these now, the underlying state model is exposed enough to wire them up yourself — but tell us so we can prioritize.
Tokens
See the tokens block in this spec’s frontmatter.
Install: @helixui/core
import { DataTable, type ColumnDef } from '@helixui/core'status: stable · since: 0.1.0
Tags: table, datagrid, list, sort, filter, pagination, selection, admin
Props
| Name | Type | Default | Description |
|---|---|---|---|
data | readonly T[] | required | The rows to display. Order is preserved unless sorting is active. |
columns | readonly ColumnDef<T>[] | required | Column definitions. Each column has an id, header, accessor, and optional sort/filter/cell renderer. |
getRowId | (row: T, index: number) => string | defaultGetRowId | Stable row identifier. Defaults to row.id if present, otherwise the row index. |
pageSize | number | undefined | Enables client-side pagination. Renders all rows if absent. |
selectable | boolean | false | Show the row-selection checkbox column. |
onSelectionChange | (ids: Set<string>) => void | undefined | Fires whenever the selection changes. |
defaultSelection | Iterable<string> | undefined | Initial selection (uncontrolled). |
selection | Set<string> | undefined | Controlled selection. Pair with onSelectionChange. |
defaultSort | { id: string; direction: 'asc' | 'desc' } | undefined | Initial sort (uncontrolled). |
sort | { id: string; direction: 'asc' | 'desc' } | null | undefined | Controlled sort. Pair with onSortChange. Use for server-side sorting. |
onSortChange | (sort: { id: string; direction: 'asc' | 'desc' } | null) => void | undefined | Fires when the user changes the active sort. |
emptyState | ReactNode | undefined | Rendered when no rows match. Defaults to a friendly “No results” message. |
loading | boolean | false | When true, replaces the body with loadingState. |
loadingState | ReactNode | undefined | Rendered while loading is true. Defaults to a “Loading…” line. |
striped | boolean | true | Alternate row backgrounds. |
bordered | boolean | true | Borders between rows. |
density | 'compact' | 'comfortable' | undefined | Overrides the density gene for this table. Useful for admin views. |
Tokens used
color.bg.surface.default, color.bg.surface.subtle, color.bg.action.brand.subtle, color.bg.action.brand.default, color.border.subtle, color.border.action.brand, color.text.primary, color.text.secondary, color.text.muted, color.text.action.brand, radius.sm, radius.md, spacing.1, spacing.2, spacing.3, spacing.4, spacing.8, font.size.xs, font.size.sm, font.weight.semibold
Accessibility
Role: table
Keyboard
| Key | Action |
|---|---|
Tab | Focus moves between sortable headers / filter inputs / checkboxes. |
Space | On a focused sort header — toggles asc → desc → off. |
ArrowKeys | Native browser scrolling inside the table region. |
Notes
- Sortable headers announce
aria-sort(ascending / descending / none). - Each row’s checkbox has a contextual
aria-label. - The scrollable container is a focusable region with a visible focus ring.
- Caption announces the total row count and active sort.