Files
cadence-ui/docs/rfcs/data-table.md
T

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:

  1. adopt a headless row-model dependency for the hard table state problems
  2. keep the public API source-owned and design-system specific
  3. ship a narrow core table first
  4. 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
  • Sheet and EmptyState are 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

  • DataTable
  • DataTableToolbar
  • DataTableSearch
  • DataTableFilters
  • DataTableContent
  • DataTableTable
  • DataTableHeader
  • DataTableHeaderCell
  • DataTableBody
  • DataTableRow
  • DataTableCell
  • DataTableEmpty
  • DataTableLoading
  • DataTablePagination
  • DataTableSelectionBar

This is intentionally a pattern family, not a single monolith.

Suggested stable slots

The following data-slot names should exist in the first implementation:

  • root
  • toolbar
  • input
  • content
  • table
  • header
  • row
  • cell
  • empty
  • pagination
  • actions

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-loading
  • data-empty
  • data-selected
  • data-sort
  • data-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

  • Input for search
  • Select and DropdownMenu for filters and view options
  • Checkbox for row selection
  • Button for primary and secondary actions
  • Badge for row metadata
  • Tooltip for truncated or status-heavy cells
  • Popover for compact detail reveal
  • Sheet for row inspection or bulk edit side panels
  • EmptyState for no-results and first-run moments
  • Skeleton for loading rows

DataTable should not try to replace

  • Sheet detail workflows
  • Dialog destructive confirmations
  • EmptyState standalone onboarding/empty moments
  • Form validation 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:

  • Playground
  • States
  • Anatomy
  • Accessibility

Likely additional stories:

  • Selection
  • Empty 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 Sheet still 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-table inside packages/ui
  • 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

  1. Should the public column type stay fully source-owned, or should the first release expose a thin alias around TanStack column defs?
  2. Should the first release include client-side search/filter state inside DataTable, or should filtering remain externally controlled?
  3. Do we want a density variant in the first release, or should row height stay fixed until real product usage appears?
  4. Should pagination UI be part of the root component family, or remain a separate companion component?
  5. 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