Table Reference
Accessibility
The renderer emits a native table with the semantics assistive technology already understands, adds ARIA only where HTML has no answer, and is tested for behavior in a browser. The application supplies the names.
- Source
- Tests
- Fixture
On this page
Native semantics
Output is <table>, <caption>, <colgroup>, <thead>, <tbody> and <tfoot>. Header
cells carry scope="col", group headers scope="colgroup", group rows
are a header cell with scope="row". Sort controls are <button>s,
selection controls are native checkboxes and radios, the editor is a native input or select.
Because the elements are native, focus, activation keys, form participation and the disabled
state come from the browser. What follows is the shape of the rendered markup with the
accessible names filled in.
<div class="slean-table" id="members">
<div class="slean-table-toolbar">
<input class="slean-table-search" type="search" aria-label="Search rows" …>
<span class="slean-table-selected" aria-live="polite">2 selected</span>
</div>
<div class="slean-table-scroll" role="region" aria-label="Members" tabindex="0"> <!-- tabindex only with height -->
<table aria-label="Members" aria-busy="false">
<caption>Members by team</caption>
<colgroup>…</colgroup>
<thead>
<tr>
<th scope="col" rowspan="1" class="slean-table-selection"><input type="checkbox" data-slean="checkbox" aria-label="Select all eligible rows"></th>
<th scope="col" data-column="name" aria-sort="ascending"><button type="button" class="slean-table-sort">Member …</button></th>
<th scope="colgroup" colspan="2">Organization</th>
</tr>
</thead>
<tbody>
<tr data-row-id="1" data-selected="true">
<td class="slean-table-selection"><input type="checkbox" data-slean="checkbox" aria-label="Select row: Amara Okafor" checked></td>
<td data-cell data-column="name">Amara Okafor</td>
</tr>
</tbody>
<tfoot>…</tfoot>
</table>
</div>
<div class="slean-table-pagination"><span aria-live="polite">Page 1 of 3 · 24 rows</span> … <button aria-label="Page 1" aria-current="page">1</button> …</div>
</div>ARIA
aria-sorton the sorted header; with several rules the precedence number is preceded by the visually hiddensortOrdermessage.aria-labelon the scroll region and the table (label), on every control (from the messages), on the pin select ("Pin: Team", not one of its options).aria-live="polite"on the selected count and the pager status;role="status"on the loading line;role="alert"on the error and the editor's validation message.aria-expandedon row and group toggles;aria-busyon the table while loading or fetching. Skeleton rows arearia-hidden; the loading message is announced instead.- The selection and expand columns are never empty headers: without a select-all checkbox the
selection header holds the visually hidden
selectionHeadermessage, and the expand column theexpandHeadermessage. - Row controls are named after the row:
rowLabelreturns the name ("Select row: Amara Okafor"); without it the row id is used. aria-current="page"on the current numbered page. In manual mode with a knownrowCount,aria-rowcounton the table andaria-rowindexon rows give the position within the whole result.- No
role="grid", noaria-selectedon rows: the native inputs already convey selection, anddata-selectedexists for styling.
Keyboard
In a plain table every control is an ordinary Tab stop. With editing or range selection the body follows the grid pattern of the ARIA Authoring Practices: one Tab stop, arrow keys inside, focus returned to the cell after an edit.
| Key | When | Result |
|---|---|---|
| Tab/Shift+Tab | anywhere | Moves through search, header buttons, resize handles, filters, row controls, the body (one stop in grid mode) and the pager. |
| Enter/Space | on a sort button | Cycles the sort; with Shift, adds the column to a multi-sort. |
| ArrowLeft/ArrowRight | on a resize handle | Narrows or widens the column by ten pixels. |
| Space | on a row checkbox | Toggles the row (native). |
| Enter/Space | on a focused row, with onRowClick | Runs onRowClick, as a click would. Rows are Tab stops only outside grid mode; keys on a control inside the row stay with the control. |
| ArrowUp/ArrowDown | on a row radio | Moves the single selection (native radio group; needs the id prop). |
| Arrow keys | on a cell in grid mode | Moves between data cells by structure; mirrored in RTL. |
| Enter/F2 | on an editable cell | Opens the editor; Enter saves and Escape cancels inside it. |
| Arrow keys/Page keys | on the height-limited region | Scrolls the region (it is a Tab stop only when a height is set). |
What the application supplies
The table cannot invent names. It needs a label that says what the rows are, a caption when the context is not obvious, column header text that
stands on its own as a control label ("Filter Team", "Pin: Team"), an id when
single selection is used, and messages in the user's language. Content inside cell snippets and the empty snippet is the application's markup and must be accessible on
its own: an icon needs a label, a control needs a name.
<DataTable
label="Open invoices" <!-- the name of the region and the table -->
caption="Invoices due this month" <!-- a visible caption -->
id="invoices" <!-- required with selection="single" -->
rowLabel={(row) => row.customer} <!-- "Select row: Acme" instead of "Select row: 42" -->
messages={de} <!-- every string in the user's language -->
…
/>Visual
- Focus uses
:focus-visiblewith the shared focus ring; it is never removed. - Selected rows and cell ranges use
HighlightandHighlightTextunderforced-colors: active; disabled controls use the platform's disabled rendering. - Motion is limited to short transitions on buttons and stops under
prefers-reduced-motion; the table has no animation of its own. - Nothing is conveyed by color alone: sort direction is an icon plus
aria-sort, selection is a checked input, validation is text.
How it is tested
The DOM suite (data-table.dom.test.ts) mounts the component under happy-dom and
checks behavior: sort on header click and the multi-sort precedence exposed to assistive
technology, checkbox and header selection, single-selection radios sharing the table id, one Tab
stop in the body with edit buttons excluded, focus returning to the cell after Escape, Cancel
and Save, row-based navigation over spans and detail rows, direction following the document, and
the scroll region being a Tab stop only when height-limited. The server suite renders the table
without a DOM and guards against browser-global access. The playground runs the same
interactions in a real browser and an axe scan; serious and critical violations fail the suite.
Screen-reader runs are not part of the suite and are not claimed.
API
| Name | Of | Type | Default | Description |
|---|---|---|---|---|
label | Presentation | string | 'Data table' | Accessible name of the scroll region and the <table>. |
caption | Presentation | string | undefined | Visible native <caption>. |
id | Data and identity | string | undefined | id of the root element. With selection: 'single' it also names the row radios (<id>-selection) so they form one native radio group. Authored, never generated. |
messages | Presentation | Partial<TableMessages> | en | Overrides of the visible and accessible strings; missing keys fall back to en. |
height | Presentation | string | undefined | max-height of the scroll region; with it the region is a Tab stop. |
selectRow | TableMessages | string | 'Select row' | Prefix of each row control label (followed by the row id). |
selectAll | TableMessages | string | 'Select all eligible rows' | Label of the header checkbox. |
selectionHeader | TableMessages | string | 'Selection' | Visually hidden header text of the selection column when it has no select-all checkbox. |
expandHeader | TableMessages | string | 'Details' | Visually hidden header text of the expand column (expandColumn). |
pageNumber | TableMessages | (page: number) => string | (page) => `Page ${page}` | Accessible name of a numbered page button. |
rowLabel | Controls | (row: T) => string | the row id | Accessible name of a row in its selection, expand and move controls. |
onRowClick | Events and composition | (row: T, event: MouseEvent | KeyboardEvent, index: number) => void | undefined | Runs for clicks outside controls (button, a, input, select, textarea, label, summary, contenteditable, menuitem, [data-action], [data-no-row-click]). Rows become Tab stops answering Enter and Space unless the table has cell navigation. |
sortOrder | TableMessages | string | 'Sort order' | Read to assistive technology before the multi-sort precedence number. |
pin | TableMessages | string | 'Pin' | Prefix of the pin select label. |
filter | TableMessages | (column: string) => string | (column) => `Filter ${column}` | Label of a column filter input. |