Skip to content

Data table (admin-grade)

components specs/components/data-table.kmd

Admin-grade tabular component — sortable columns, multi-select with bulk actions, sticky header, pagination or virtualization, saved column views, expandable rows, inline edit. Modeled after Polaris IndexTable, PatternFly Table+Toolbar, Material 3 DataTable.

When this spec applies

Primary triggers

All triggers

Specification body

Component — Data table

Status: v0.1.0 — Draft. Behavioral contract; rendering choice (HTML table vs virtualized list) is implementation detail.

R1 — Column sort

  • Single-column sort: click header → ascending; click again → descending; click again → unsorted.
  • Multi-column sort: Shift+click adds secondary sort; numeric badge shows priority.
  • Sortable columns marked with directional arrow icon; unsortable columns omit the icon.

R2 — Row selection

  • Single-select mode: single column of radio buttons.
  • Multi-select mode: single column of checkboxes; header checkbox toggles all visible.
  • Range select: Shift+click checkbox extends selection from last clicked.
  • Selection state announced via live region: "{N} of {total} rows selected".

R3 — Bulk-action bar

  • When ≥ 1 row selected, the column-header row morphs into a bulk-action bar:
    • Left: {N} selected + Clear button.
    • Right: action buttons (Delete, Move, Export, …).
  • Animated slide-in (200ms; prefers-reduced-motion: instant swap).
  • Bar is sticky alongside the header during scroll.

R4 — Sticky header

  • Header row stays visible during vertical scroll within the table viewport.
  • On nested-page scroll past the table top, header may pin to viewport top (page-level decision; not all surfaces want this).

R5 — Pagination vs virtualization (decision tree)

Row countUse
< 50No pagination; render all
50–500Pagination (25/50/100 per page)
500–5,000Cursor pagination + load-more button
> 5,000Virtualization (windowed render); show count + filter to drive down

R6 — Saved views

  • User saves named view: name + columns + sort + (filters from specs/components/index-filters.kmd).
  • Views persisted per-user + per-table-identity.
  • View dropdown in toolbar; default view always present.

R7 — Expandable rows

  • Optional per-table: row caret toggles inline expansion.
  • Expanded content lives inside the row; not modal.
  • Only one row expanded at a time (accordion) OR multiple (parallel) — table-level config.

R8 — Inline edit

  • Optional per-column: cell focus → field edit mode.
  • Enter commits; Esc reverts; Tab commits + moves to next editable cell.
  • Live region announces commit success ("Saved: {field} = {value}") or failure.

R9 — Keyboard nav

KeyAction
TabMove through interactive cells (links, buttons, selects)
Shift+TabReverse
↑/↓Navigate row by row
←/→Navigate cell by cell within a row
Space (on row checkbox cell)Toggle selection
Enter (on row)Activate primary row action (open detail)

R10 — Screen-reader semantics

  • <table> element with <caption> (table name) or aria-label.
  • <th scope="col"> for column headers; aria-sort reflects sort state.
  • <tr> for row; selected rows have aria-selected="true".
  • Bulk action bar replaces header semantically (announces via live region).

R11 — OUIA hooks

Per specs/testing/ouia-test-hooks.kmd:

  • data-ouia-component-type="DataTable"
  • data-ouia-component-id="<table-slug>"
  • data-ouia-safe="true" only when data loaded + no pending edits.

R12 — Empty / loading

  • Empty: render per specs/patterns/empty-state.kmd inside the table container (no row stripes).
  • Loading: render per specs/components/skeleton.kmd (5 skeleton rows mirroring final column layout).

Não-escopo

  • Server-side data fetching (transport-agnostic).
  • Specific virtualization library (TanStack Virtual, react-window, etc.).
  • Charting overlays (separate spec if needed).
  • Pro-layer wrapping (search bar + saved views + density toggle + column visibility menu + export bundled as a single component) — evaluated in design-RFC-008 and ratified Option C (recipe pattern only) on 2026-05-23. Consumers compose the toolbar themselves following specs/patterns/admin-data-table.kmd; KDS does NOT ship a bundled Pro component.

Requirements (testable)

Requirement: Column sort cycle {#req-data-table-column-sort}

A sortable column header SHALL cycle its sort state on successive activation in the order ascending → descending → unsorted, and Shift+activation SHALL add the column as a secondary sort key with a visible numeric priority badge, while unsortable columns SHALL NOT expose a sort control.

Scenario: Single-column three-state cycle

  • GIVEN a data table with a sortable column "Name" in the unsorted state
  • WHEN the user activates the "Name" header three times in succession
  • THEN the first activation SHALL set the column to ascending
  • AND the second activation SHALL set it to descending
  • AND the third activation SHALL return it to unsorted

Scenario: Multi-column sort with priority badge

  • GIVEN a table already sorted ascending by "Name"
  • WHEN the user Shift+activates the "Created" column header
  • THEN "Created" SHALL become a secondary sort key
  • AND a numeric priority badge SHALL be shown reflecting sort order (Name=1, Created=2)

Scenario: Unsortable column exposes no control

  • GIVEN a table with a column configured as unsortable
  • WHEN the header row is rendered
  • THEN the unsortable column SHALL omit the directional sort arrow icon
  • AND activating that header SHALL NOT change any column's sort state

Requirement: Row selection and announcement {#req-data-table-row-selection}

In multi-select mode the table SHALL render a checkbox column whose header checkbox toggles all visible rows, SHALL extend the selection contiguously when a checkbox is Shift+clicked from the last clicked row, and SHALL announce the current selection count through a live region as "{N} of {total} rows selected".

Scenario: Header checkbox toggles all visible rows

  • GIVEN a table in multi-select mode showing 10 rows with none selected
  • WHEN the user activates the header checkbox
  • THEN all 10 visible rows SHALL become selected
  • AND the live region SHALL announce "10 of {total} rows selected"

Scenario: Range select via Shift+click

  • GIVEN row 2 was the last checkbox clicked and is selected
  • WHEN the user Shift+clicks the checkbox on row 6
  • THEN rows 2 through 6 inclusive SHALL be selected
  • AND the live region SHALL announce "5 of {total} rows selected"

Requirement: Bulk-action bar on selection {#req-data-table-bulk-action-bar}

When one or more rows are selected the column-header row SHALL be replaced by a bulk-action bar showing "{N} selected" with a Clear control on the left and action buttons on the right, the bar SHALL remain sticky alongside the header during scroll, and its transition SHALL be an instant swap when prefers-reduced-motion is set.

Scenario: Header morphs into bulk-action bar

  • GIVEN a table with zero rows selected showing the normal column header
  • WHEN the user selects at least one row
  • THEN the header row SHALL be replaced by the bulk-action bar
  • AND the bar SHALL display "{N} selected", a Clear control, and the action buttons

Scenario: Reduced motion swaps instantly

  • GIVEN the environment reports prefers-reduced-motion: reduce
  • WHEN the bulk-action bar appears on first selection
  • THEN the bar SHALL swap in instantly without the 200ms slide-in animation

Requirement: Sticky header during scroll {#req-data-table-sticky-header}

The header row SHALL stay visible at the top of the table viewport while the table body is scrolled vertically, so that column headers remain associated with the rows currently in view.

Scenario: Header remains visible on vertical scroll

  • GIVEN a table whose body overflows its viewport
  • WHEN the user scrolls the body down past the first screenful of rows
  • THEN the header row SHALL remain pinned and visible at the top of the table viewport

Requirement: Keyboard navigation {#req-data-table-keyboard-nav}

The table SHALL support keyboard operation such that arrow keys move focus row-by-row (↑/↓) and cell-by-cell within a row (←/→), Space on a row's checkbox cell toggles that row's selection, and Enter on a row activates its primary action, with Tab and Shift+Tab moving through interactive cells.

Scenario: Arrow keys move focus by row and cell

  • GIVEN keyboard focus is on a cell in the first row
  • WHEN the user presses ↓ and then →
  • THEN ↓ SHALL move focus to the corresponding cell in the next row
  • AND → SHALL move focus to the next cell within that row

Scenario: Space toggles selection and Enter activates row

  • GIVEN keyboard focus is on an unselected row's checkbox cell
  • WHEN the user presses Space and then presses Enter on the row
  • THEN Space SHALL toggle that row into the selected state
  • AND Enter SHALL activate the row's primary action (open detail)

Requirement: Screen-reader semantics {#req-data-table-sr-semantics}

The table SHALL expose accessible semantics: a <table> with a <caption> or aria-label naming it, <th scope="col"> column headers whose aria-sort reflects the current sort state, and selected rows carrying aria-selected="true".

Scenario: Sort state reflected in aria-sort

  • GIVEN a table with column headers marked up as <th scope="col">
  • WHEN the "Name" column is sorted ascending
  • THEN the "Name" header SHALL carry aria-sort="ascending"
  • AND the table SHALL expose a caption or aria-label naming the table

Scenario: Selected row exposes aria-selected

  • GIVEN a table in multi-select mode with a given deselected row
  • WHEN the user selects that row and then deselects it
  • THEN selecting SHALL make that row's <tr> carry aria-selected="true"
  • AND deselecting SHALL set that row's aria-selected to false or remove the attribute

References