Design System
Patterns & Recipes

How do I build a table?

TanStack Table, via ui-kit's DataTable — the one true table.

Prescribed: TanStack Table via DataTable. Don't reach for another table library or hand-roll sort/pagination state — DataTable already wraps TanStack's headless engine with this design system's responsive layout (a real <table> on desktop, stacked cards on phone), sortable headers with correct aria-sort, and built-in loading/error states.

The approved implementation

Define your rows' shape and a plain TanStack ColumnDef[], then hand both to DataTable with an accessible caption. Sorting, responsive column hiding (via each column's meta.responsiveClassName), and the loading/error states all come for free — you don't wire any of it yourself.

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

When to reach for something else

  • Rows can be selected (bulk actions, checkboxes) → SelectableDataTable builds on the same DataTable engine with a selection column already wired.
  • Just showing a handful of static key/value rows, no sorting or responsiveness needed → the plain Table primitives are lighter-weight and enough.
  • Metric/KPI rows with a trend column → MetricTable composes DataTable with a DeltaText change column already built in.

Read-only tables vs. permission-gated actions

These are two different things, and mixing them up reads as a bug to the user.

  • The surface has no write actions at all. Nothing is offered because nothing belongs here — the screen is a search-and-view surface and editing lives elsewhere. Render no action column and no save bar. Don't render disabled buttons as a hint that editing exists somewhere; the user can't tell a permanently-absent capability from one they're merely not allowed to use. Example: the Cost and Selling tabs in Pages/Price Entries.

  • The action exists, but this user may not perform it. Render it shown but disabled, with a restrictedReason explaining why — see RestrictedIconButton. Removing it hides the shape of the product and makes permissions look like missing features. Example: Edit/Delete on the Price Seasons table, which only the Product Team may use.

The test to apply: would a differently-permissioned user see this control enabled? Yes → show it disabled. No, nobody would → leave it out.

Do / Don't

  • Do pass a real, accessible caption — it's rendered as a visually-hidden <caption>, not decoration.
  • Do use isLoading/error/onRetry for anything backed by a real fetch — see the Data-fetching recipe for the server side of this.
  • Don't implement your own sort state, pagination, or a second table component for "just this one case" — extend DataTable's columns instead; that's exactly what its meta escape hatch is for.

On this page