Skip to content

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/tr exposed 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.sort and filter — 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

NameTypeDefaultDescription
datareadonly T[]requiredThe rows to display. Order is preserved unless sorting is active.
columnsreadonly ColumnDef<T>[]requiredColumn definitions. Each column has an id, header, accessor, and optional sort/filter/cell renderer.
getRowId(row: T, index: number) => stringdefaultGetRowIdStable row identifier. Defaults to row.id if present, otherwise the row index.
pageSizenumberundefinedEnables client-side pagination. Renders all rows if absent.
selectablebooleanfalseShow the row-selection checkbox column.
onSelectionChange(ids: Set<string>) => voidundefinedFires whenever the selection changes.
defaultSelectionIterable<string>undefinedInitial selection (uncontrolled).
selectionSet<string>undefinedControlled selection. Pair with onSelectionChange.
defaultSort{ id: string; direction: 'asc' | 'desc' }undefinedInitial sort (uncontrolled).
sort{ id: string; direction: 'asc' | 'desc' } | nullundefinedControlled sort. Pair with onSortChange. Use for server-side sorting.
onSortChange(sort: { id: string; direction: 'asc' | 'desc' } | null) => voidundefinedFires when the user changes the active sort.
emptyStateReactNodeundefinedRendered when no rows match. Defaults to a friendly “No results” message.
loadingbooleanfalseWhen true, replaces the body with loadingState.
loadingStateReactNodeundefinedRendered while loading is true. Defaults to a “Loading…” line.
stripedbooleantrueAlternate row backgrounds.
borderedbooleantrueBorders between rows.
density'compact' | 'comfortable'undefinedOverrides 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

KeyAction
TabFocus moves between sortable headers / filter inputs / checkboxes.
SpaceOn a focused sort header — toggles asc → desc → off.
ArrowKeysNative 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.