13 KiB
RFC: DataTable
Status
Implemented
The first DataTable slice now ships in packages/ui with stories, tests, and registry
distribution support. This RFC remains as the design record for the shipped scope and the
guardrails for future follow-up work.
Summary
DataTable is now a shipped advanced pattern for @ai-ui/ui, and it should continue to avoid
turning into a giant "kitchen sink" component.
The recommended path is:
- adopt a headless row-model dependency for the hard table state problems
- keep the public API source-owned and design-system specific
- ship a narrow core table first
- layer richer behaviors only after the base contract stabilizes
The recommended dependency choice is @tanstack/react-table, but only as an internal implementation detail. Consumers should build against @ai-ui/ui, not against TanStack APIs directly.
Why now
The repo has already completed most of the preconditions that the roadmap called out before advanced patterns:
- token system is in place
- core component layer is in place
- Storybook coverage exists across the current primitives
- interaction and smoke coverage are present
SheetandEmptyStateare now available as adjacent workflow patterns
Relevant current building blocks already exist in packages/ui:
- input and form controls:
Input,Select,Checkbox,RadioGroup,Switch,Form - structure and feedback:
Card,Badge,Alert,Separator,Skeleton,Progress - overflow and contextual actions:
DropdownMenu,Popover,Tooltip,Sheet,Dialog,Toast - empty and first-run states:
EmptyState
At RFC authoring time, the main missing piece for list-heavy product surfaces was not another primitive. It was a stable data presentation pattern.
Problem statement
Before this work, the design system could express forms, overlays, command/search, and empty states, but it could not yet express a reusable operational list surface such as:
- release queues
- approval backlogs
- rollout logs
- audit result lists
- environment health tables
- reviewer assignments
Without a DataTable pattern, application code will drift into one-off table wrappers with inconsistent:
- toolbar layout
- filter/search interactions
- row selection behavior
- loading and empty states
- keyboard behavior
- sticky headers and scrolling decisions
- bulk action affordances
- pagination treatment
This is exactly the kind of "parallel wrapper" problem the roadmap warns against.
Non-goals
The first implementation should not attempt to solve every table problem.
Explicit non-goals for the first slice:
- virtualization
- column resizing
- column reordering
- nested tree rows
- grouped/aggregated rows
- drag and drop
- inline cell editing
- Excel-style spreadsheet behavior
- server transport concerns
- generic charting or pivot-table behavior
Those can come later if real product surfaces prove they are needed.
Current repo constraints
This RFC is anchored in the current repo, not an idealized future state.
Architectural constraints
- Components are source-owned and exported from
packages/ui/src/index.ts - Styling is token-first and stateful styling flows through stable
data-*attributes - Slot naming is already standardized in
packages/ui/src/lib/contracts.ts - Motion should be purposeful and reduced-motion safe
- Stories are expected to explain anatomy and behavior, not just render a demo
- QA already uses
Vitest, Storybook interaction coverage, and Playwright smoke
Dependency constraints
When this RFC was written, packages/ui/package.json had no table model dependency, no date
library, and no grid engine.
That meant the first DataTable implementation had to either:
- hand-roll row modeling, sorting, selection, and visibility management, or
- adopt a focused headless dependency
Hand-rolling was not recommended for this repo stage, and the shipped implementation now uses
@tanstack/react-table internally.
Recommendation
Use @tanstack/react-table internally for the first DataTable implementation.
Why TanStack Table
It solves the complex headless data problems we do not want to rediscover:
- row modeling
- sorting state
- filtering state
- pagination state
- selection state
- column visibility
- stable cell/header modeling
This matches the repo's principle of using a strong interaction/state base under a source-owned UI layer, the same way Radix underpins overlays and controls.
Why not build it from scratch
Building even a "simple" table pattern from scratch will immediately force the repo to own:
- sorting semantics
- accessor APIs
- row identity rules
- selection bookkeeping
- controlled vs uncontrolled state design
- column metadata normalization
That is too much API design surface for a first table release.
Why not expose TanStack directly
Exposing TanStack types and concepts as the public API would weaken the repo's current direction:
- it would leak a third-party mental model into consumer code
- it would make docs look like a wrapper around someone else's library
- it would make future refactors harder
The public contract should stay @ai-ui/ui first. TanStack should remain an implementation detail wherever possible, with any unavoidable type exposure deliberately minimized.
Proposed scope: Stage 1 core
The first shipping slice should cover the operational table use case, not the spreadsheet use case.
Must-have behaviors
- typed columns
- sortable columns
- optional row selection
- optional client-side text filter
- empty state rendering
- loading state rendering
- pagination controls
- column-level cell formatting
- row-level actions slot
Must-have supporting surfaces
- toolbar for search, filters, and view actions
- sticky or visually distinct header treatment
- bulk selection affordance when selection is enabled
- responsive overflow strategy
Proposed public API shape
The API should be compositional and stable, following the repo's current component contract.
Root component
<DataTable
columns={columns}
rows={rows}
getRowId={(row) => row.id}
loading={loading}
empty={<DataTableEmpty ... />}
searchValue={search}
onSearchValueChange={setSearch}
selection={selection}
onSelectionChange={setSelection}
sorting={sorting}
onSortingChange={setSorting}
/>
Suggested slot family
DataTableDataTableToolbarDataTableSearchDataTableFiltersDataTableContentDataTableTableDataTableHeaderDataTableHeaderCellDataTableBodyDataTableRowDataTableCellDataTableEmptyDataTableLoadingDataTablePaginationDataTableSelectionBar
This is intentionally a pattern family, not a single monolith.
Suggested stable slots
The following data-slot names should exist in the first implementation:
roottoolbarinputcontenttableheaderrowcellemptypaginationactions
Additional slot names should only be added when they create a durable styling or testing hook.
Suggested stable state surface
The following public state hooks are likely worth exposing:
data-loadingdata-emptydata-selecteddata-sortdata-density
data-state should still be used for finite row/header states where that is the clearest representation.
Proposed column model
The public API should accept a narrow column definition owned by this repo, even if it is internally adapted to TanStack.
Example direction:
type DataTableColumn<TData> = {
id: string;
header: ReactNode | ((column: DataTableColumnContext<TData>) => ReactNode);
cell: (row: TData) => ReactNode;
accessor?: keyof TData | ((row: TData) => unknown);
sortable?: boolean;
align?: "start" | "center" | "end";
width?: number | string;
priority?: "primary" | "supporting" | "low";
};
Key point:
- keep the first public column shape small
- do not surface every TanStack option
- do not make users learn a second full configuration language on day one
Composition model
The table should integrate naturally with the patterns the repo already ships.
DataTable should compose with
Inputfor searchSelectandDropdownMenufor filters and view optionsCheckboxfor row selectionButtonfor primary and secondary actionsBadgefor row metadataTooltipfor truncated or status-heavy cellsPopoverfor compact detail revealSheetfor row inspection or bulk edit side panelsEmptyStatefor no-results and first-run momentsSkeletonfor loading rows
DataTable should not try to replace
Sheetdetail workflowsDialogdestructive confirmationsEmptyStatestandalone onboarding/empty momentsFormvalidation and edit flows
The table is the list surface. Other patterns handle adjacent work.
Accessibility requirements
The first release should treat accessibility as a primary contract, not a follow-up.
Baseline requirements
- semantic table structure for the standard grid use case
- keyboard reachable sort controls
- keyboard reachable row selection controls
- visible focus styling on row actions and interactive header controls
- accessible labeling for search, filters, and pagination controls
- empty and loading states that remain understandable to screen readers
- no motion dependency for critical state changes
First-release accessibility decisions
- Prefer semantic HTML table markup for the default implementation
- Do not start with
role="grid"unless we truly need spreadsheet-like keyboard control - Treat sorting controls as buttons inside header cells
- Keep row selection explicit with
Checkbox, not hidden click-to-select rows
This fits the repo's current accessibility posture better than jumping straight to a complex grid interaction model.
QA expectations
The table should meet the repo's current QA bar from the first release.
Unit and interaction coverage
Minimum expected coverage:
- renders header, rows, and cells with stable slots
- sortable columns update controlled and uncontrolled sort state
- selection toggles update row state and bulk action surface
- loading state renders skeleton or loading rows
- empty state renders when no rows remain after filtering
- search/filter plumbing updates the rendered row set
- pagination changes page and respects bounds
Storybook coverage
Minimum story recipe:
PlaygroundStatesAnatomyAccessibility
Likely additional stories:
SelectionEmpty and Loading
Playwright smoke
At least one smoke scenario should cover:
- table renders
- search narrows rows
- sorting changes row order
- selecting rows reveals a bulk-action affordance
- opening row detail in
Sheetstill works
Original release plan
Stage 0: RFC and composition rehearsal
- finalize API direction in this RFC
- build one realistic docs composition page that uses a table-like operational surface
- identify missing supporting primitives before table code starts
Stage 1: Headless core
Ship a narrow DataTable that supports:
- typed columns
- sorting
- selection
- search
- pagination
- loading and empty states
No virtualization, resizing, pinning, or editing.
Stage 2: Workflow integration
Add only after Stage 1 stabilizes:
- row action menu patterns
- bulk action bar
- column visibility menu
- row details opening in
Sheet
Stage 3: Scale features
Only if justified by real product usage:
- server-driven pagination hooks
- column pinning
- virtualization
- advanced filters
Implemented file layout
The initial table slice now lives in:
packages/ui/src/components/data-table.tsx
packages/ui/src/components/data-table.variants.ts
packages/ui/src/components/data-table.test.tsx
apps/docs/src/components/data-table.stories.tsx
If the implementation grows into a larger family, the repo may eventually want a dedicated component folder, but the current slice still follows the flat component layout.
Dependency recommendation
Current choice
- use
@tanstack/react-tableinsidepackages/ui
Not recommended for the first slice
- virtualization package
- drag and drop package
- date package for table filters
- full export/import package support
The first implementation should minimize dependency expansion and keep the complexity budget focused on a single new capability.
Follow-up questions
- Should the public column type stay fully source-owned, or should the first release expose a thin alias around TanStack column defs?
- Should the first release include client-side search/filter state inside
DataTable, or should filtering remain externally controlled? - Do we want a density variant in the first release, or should row height stay fixed until real product usage appears?
- Should pagination UI be part of the root component family, or remain a separate companion component?
- Should row selection be opt-in only, or should the root always reserve structure for it?
Result
The shipped DataTable implementation:
- is built as an advanced pattern, not a primitive
- is source-owned at the public API layer
- uses TanStack Table internally from the first implementation
- ships as a narrow, headless/core-first operational table
- defers scale features until real usage proves they are necessary