Design System
Components

Data Table

Sortable, responsive table on TanStack Table — the foundation under every other table in the system.

The headless TanStack engine rendered through the design system's semantic table primitives. Click a sortable column header — sorting, loading skeletons, and the failed-state view are handled for you, not something each screen re-implements.

  • RouteFirst Passage to the West
    Days2
    ServiceGoldLeaf
  • RouteJourney through the Clouds
    Days2
    ServiceSilverLeaf
  • RouteRainforest to Gold Rush
    Days2
    ServiceGoldLeaf

Props

Prop

Type

Implementation notes

  • Responsive by layering, not by hiding the table. Below the md breakpoint the same sorted row model renders as a stacked card list instead of a horizontally-scrolled table — set a column's meta.responsiveClassName (e.g. "hidden lg:table-cell") to drop a low-priority column above md, rather than trying to cram every column into a phone width.
  • error replaces the table, it doesn't sit beside it. Pass a string to error (with onRetry) and the component swaps in an <ErrorState>. Use errorTitle and retryLabel when the default copy is too generic; don't build a separate error branch around <DataTable> in the consumer.
  • Sortable headers announce their state. A sortable column's <th> carries aria-sort ("ascending"/"descending"/"none") automatically from TanStack's sort state — this is why sorting should stay wired through the column def's enableSorting/TanStack sort state rather than a hand-rolled click handler that bypasses it.
  • Column behavior can be controlled. Pair columnOrder with onColumnOrderChange, and columnSizing with onColumnSizingChange. Set enableColumnReordering and enableColumnResizing to expose the controls. stickyHeader keeps headings visible inside a vertically scrolling table region bounded by maxHeight.
  • Empty content is built in. Set emptyTitle, with optional emptyDescription and emptyAction, instead of branching around the table. Loading, empty and error then share one component contract.

Accessibility

  • Give every table a real caption — it renders as a visually-hidden <caption>, not decoration.
  • Move buttons and resize separators have table-specific accessible labels. Resize separators accept Left/Right Arrow in 16-pixel steps, while reorder buttons announce the resulting position. Pointer resizing remains available. These controls are separate from the sort button, so resizing or moving a column does not change its sort direction.
  • Set meta.controlLabel when two columns intentionally share a visible heading. The unique label is used by resize and reorder controls while the visible text stays faithful to the product language.
  • getRowId is optional but strongly recommended whenever rows can reorder (sorting, refetch) — without it, React keys by array index, which can silently misattribute state (e.g. row selection) to the wrong row after a re-sort.

Do / Don't

  • Do reach for SelectableDataTable instead of hand-adding a checkbox column here — it's the same table with row selection layered on, not a fork.
  • Do pass isLoading rather than conditionally rendering the table only once data arrives — the skeleton keeps the column layout stable instead of the page reflowing when data lands.
  • Do keep sizing in controlled state for the current view when users may resize columns. Browser cache restore applies to column order and sort only in RMS-1557; persisted widths and cross-device preferences are separate product work.
  • Don't implement pagination inside a <DataTable> consumer's column defs or row model — pagination is a concern of what data you pass in, not of this component. A numbered pager suits an offset/page-indexed API; an opaque-cursor API needs a different pager shape entirely (prev/next only, no page count) — don't force one pattern onto the other.

On this page