Skip to content
SuperCoach

Components · Data display

Data table

Beta

A table with sorting, filtering and selection, such as the client list.

Clients, sorted by the column marked as sorted
ProgramStatusActions
Abdullah Al-Zahrani12-week strength16 Oct 2026ActiveSAR 1,215
Aisha Al-BalushiFat loss starter20 Oct 2026ActiveSAR 600
Ali Al-AjmiHypertrophy block18 Oct 2026ActiveSAR 800
Badr Al-Otaibi12-week strength10 Oct 2026ActiveSAR 450
Dana Al-KuwariFat loss starter19 Oct 2026ActiveSAR 600
Faisal Al-Harbi12-week strength28 Sep 2026PausedSAR 450
Hamad Al-MarriHypertrophy block3 Oct 2026Payment overdueSAR 1,215
Huda Al-MutairiFat loss starter5 Oct 2026Payment overdueSAR 600
Jana Al-RashidNutrition coaching17 Oct 2026ActiveSAR 350
Khalid Al-QahtaniHypertrophy block19 Oct 2026ActiveSAR 800

Showing 1–10 of 24

When to use

  • For long lists that coaches work through: the client list, invoices, payouts, check-ins to review.
  • When coaches need to sort, filter, select several rows for one action, or page through the results.

When not to use

  • For a short, fixed list that needs none of that: use a plain Table.
  • For a few items with rich content, such as programs with a cover and a description: use Cards.
  • When there is no data yet: show an Empty state instead of an empty table with headers.

Anatomy

A data table is a composition, not one component, following shadcn's data-table recipe. TanStack Table (@tanstack/react-table v9, installed) owns the state: sorting, filtering, selection and pages. The markup is the Table component, with Button, Checkbox, Input and Dropdown menu inside it. There is no components/ui/data-table.tsx; copy the example and change the columns.

StatusMonthly feeActions
Sara Al-OtaibiActiveSAR 450
Omar HaddadPayment overdueSAR 600

Showing 1–10 of 24

  1. 1. Toolbar: a filter with a label (visually hidden), bulk actions when rows are selected
  2. 2. Row checkbox, with "select all" in the header (checked, unchecked or mixed)
  3. 3. Sortable header: a button inside the th, which carries aria-sort
  4. 4. Row actions: an icon button that opens a menu, named after the row
  5. 5. Count: "Showing 1–10 of 24", a live region
  6. 6. Pagination: Previous and Next buttons
PartBuilt withTanStack feature
TableTable, TableHeader, TableRow, TableHead, TableCelluseTable, getHeaderGroups(), getRowModel()
SortingButton (ghost, sm) in TableHeadrowSortingFeature, createSortedRowModel()
FilterLabel + Input type="search"columnFilteringFeature, createFilteredRowModel()
SelectionCheckboxrowSelectionFeature, getRowId
Row actionsDropdownMenu + Button (ghost, icon-sm)—
PaginationButton (outline, sm)rowPaginationFeature, createPaginatedRowModel()

In TanStack Table v9, every feature is registered once in tableFeatures({ … }) at module scope, together with its row model. Keep features, columns and the data stable (module scope or useMemo), or the row models are rebuilt on every render.

Variants

One pattern. Leave out the parts a list doesn't need: a payout history has no selection, and a list of 8 rows has no pagination.

  • Client-side: all rows are loaded, as in the example. Fine up to a few hundred rows.
  • Server-side: the API sorts, filters and pages. Set manualSorting, manualFiltering and manualPagination, and pass rowCount so "Showing 11–20 of 1,240" stays right.

States

StateWhat changesToken
Row hoverLight fill, to follow a row acrossbg-muted/50
Row selectedMuted fill, data-state="selected", checkbox checkedbg-muted; checkbox bg-primary, text-primary-foreground
Some rows selectedHeader checkbox is mixed (aria-checked="mixed"); bulk actions appear in the toolbarbg-primary
Sorted columnArrow up or down replaces the neutral icon; aria-sort on the headertext-muted-foreground for the neutral icon
Focus-visibleRing on the sort button, checkbox, menu button or pagination buttonring-ring/50, 3px
No resultsOne row across all columns with what to do nexttext-muted-foreground
LoadingKeep the header, show Skeleton rows at the same height, set aria-busy="true" on the table—
Pagination at an endPrevious or Next is disabledopacity-(--opacity-disabled)

Usage guidelines

  • The first data column is what coaches scan for, usually the client's name. Sort by it by default, and say so.
  • Align text to the start and numbers to the end with tabular-nums, as in Table.
  • Make only the columns worth sorting sortable: names, dates and amounts. A status column is easier to filter than to sort.
  • Put actions for many rows in the toolbar ("Send check-in reminder") and actions for one row in its menu. Don't show both checkboxes and per-row buttons for the same action.
  • Keep the page size steady (10, 25 or 50). Reset to the first page when the filter changes.
  • Destructive actions in the row menu use the destructive item and open a confirmation.
Do.The header text is a real button, and the th carries aria-sort. Keyboard and screen reader users can sort.
Client
Don't.A click handler on the text or the icon only. It can't be reached with Tab, and the sort order is never announced.

Showing 1–10 of 24

Do.Say where the coach is in the list, and what the total is after filtering.

Page 1

Don't.A page number alone. Coaches can't tell how much is left, or that a filter hid rows.

Content guidelines

  • Header labels are short nouns: "Client", "Program", "Last check-in", "Monthly fee". Put the unit or currency in the cell ("SAR 450"), as in Content › Money.
  • The filter's label says what it searches: "Filter by client name", not "Search".
  • The no-results row names the fix: "No clients match this name. Check the spelling or clear the filter."
  • The count reads "Showing 1–10 of 24". In Arabic, write "عرض 1 إلى 10 من 24": a dash between two numbers is reordered in right-to-left text.
  • Row action labels are verbs: "View profile", "Send check-in reminder", "Pause coaching".

Accessibility

KeyDoes
TabMoves through the filter, sort buttons, checkboxes, row menus and pagination, in reading order
Enter or Space on a sort buttonSorts by that column; pressing again reverses the order
Space on a checkboxSelects or clears the row; on the header checkbox, the whole page
Enter, Space or Arrow down on a row menu buttonOpens the menu and focuses the first item
Arrow up / Arrow down in the menuMoves between items
EscapeCloses the menu and returns focus to its button
  • Table semantics stay native. Rows and cells are <tr>, <th> and <td>, so screen readers announce the column with each cell ("Monthly fee, SAR 450"). Don't use role="grid" unless you also build grid keyboard navigation.
  • Sorting. Each sortable header holds a <button> whose name is the column name. The <th> gets aria-sort="ascending" or "descending" on the sorted column only. The arrow icons are aria-hidden. Because aria-sort changes are not announced reliably, the example also writes "Sorted by Client, ascending" to a visually hidden role="status" region. NVDA reads the sorted header as "Client, column header, sorted ascending, button".
  • Selection. Every checkbox has a name: "Select all clients on this page", "Select Sara Al-Otaibi". A partly selected page sets the header checkbox to "indeterminate", announced as "mixed". Checkboxes are 16px; they meet WCAG 2.5.8 through the spacing around them, so don't put two in one cell.
  • Row actions. The menu button is named after the row ("Actions for Sara Al-Otaibi"), not just "More", because a screen reader lists all 10 of them.
  • Filter and pages. The count is a role="status" region, so typing in the filter or changing the page announces "Showing 1–5 of 5".
  • Caption. Give the table a caption (here sr-only) so it can be found in a list of tables.
  • When the table is wider than the screen, its container scrolls and takes focus (see Table).

RTL

  • Columns run right to left: the checkbox column is on the right and the row menu on the left. text-start and text-end follow; amounts stay aligned to the end.
  • The Previous and Next chevrons mirror (rtl:-scale-x-100); sort arrows point up and down and don't.
  • The row menu opens with align="end", so it lines up with the menu button on either side.
  • Client names and amounts stay in their own script: Western digits in both languages (QUESTIONS Q22).

Tokens used

The data table adds no tokens of its own. These are the tokens of the Table component it is built on; see Button, Checkbox and Input for the rest.

Read from components/ui/table.tsx at build time, so this list changes when the component does. Opacity modifiers such as /90 are shadcn hover and focus tints on top of the token.

Semantic color tokens used by this component
TokenUtilitiesSuperCoachCoach
--foregroundtext-foregroundneutral-950neutral-950
--mutedbg-muted/50 bg-mutedneutral-100neutral-100
--muted-foregroundtext-muted-foregroundneutral-600neutral-600
--ringring-ringbrand-600orange-500
  • Radiusrounded-sm
  • Typefont-medium
  • Typetext-sm

Props

The props below are the Table parts. Sorting, filtering, selection and pagination are options of useTable; see the TanStack Table API.

<Table>

Also takes every prop of <table>

No props of its own.

<TableRow>

Also takes every prop of <tr>

No props of its own.

<TableHead>

Also takes every prop of <th>

No props of its own.

<TableCell>

Also takes every prop of <td>

No props of its own.

Replaces in coach-admin

From the read-only audit of coach-admin: 40 style values, 51 uses.

coach-admin's sortable lists use react-table v6 (.ReactTable, .rt-* classes, wrapped in QSReactTable) and hand-built tables in SC Pay and billing. Plain, non-interactive tables are listed on the Table page.

Classes and selectors

  • .rt-table5
  • .rt-th4
  • .rt-thead3
  • .rt-td3
  • .ReactTable .rt-thead .rt-th2
  • .rt-th:first-child2
  • .rt-td, .rt-th2
  • .rt-tbody2
  • .rt-th:first-child, .rt-td:first-child2
  • .ReactTable .rt-thead.-header2
  • .ReactTable .rt-th1
  • .ReactTable .rt-thead .rt-tr1

Files

  • src/views/ScPayPayouts/components/LedgerTable.tsx8
  • src/views/ScPayPayouts/components/CoachesTable.tsx3
  • src/reusableComponents/QSComponents/QSReactTable.js2
  • src/views/Settings/BillingTab/PaymentHistoryTable.tsx1

The inventory lists the first 12 locations of each value, so file counts are a lower bound.

SCSS variables in these styles

  • $paid-experience-radius-mdnot a color: spacing, size or layout
  • $border-color→ --border
  • $scroll-thumb-color→ nearest neutral-300 (no semantic token)

Table, Checkbox, Badge, Empty state, Pagination, Content.