Appearance
Grid mode & keyboard
DataTable is an accessible table by default and an opt-in WAI-ARIA grid when you set interactionMode="grid". This page covers both the grid interaction contract and the underlying accessibility surface.
Grid interaction mode
By default DataTable is an accessible table — Tab steps between the native controls (sort buttons, checkboxes, filters, pagination). Set interactionMode="grid" to opt into the full WAI-ARIA grid pattern, where the whole grid is a single tab-stop and arrow keys move a roving active cell across both axes:
rozie
<DataTable :data="$data.rows" interactionMode="grid" @activecell-change="onMove($event)">
<Column field="name" header="Name" sortable />
<Column field="email" header="Email" />
<Column field="status" header="Status" sortable />
</DataTable>What flips on:
- Roles. The root becomes
role="grid"and body cells becomerole="gridcell"(headers stayrole="columnheader").'table'mode keepsrole="table"/role="cell", byte-for-byte unchanged. - Roving tab-stop. Exactly one cell carries
tabindex="0"at a time; the rest aretabindex="-1". Tab moves focus into the grid (landing on the active cell) and a second Tab moves out — the grid is one stop in the page tab order, not one-stop-per-cell. There is no focus-steal on mount: the entry cell waits for the first Tab/click. - 2-D keyboard navigation (APG).
ArrowLeft/Right/Up/Downmove one cell;Home/Endjump to the row's first/last cell;Ctrl+Home/Ctrl+Endjump to the first/last cell of the grid;PageUp/PageDownjump by a row page.ArrowUpfrom the first body row crosses into the header row. Every index is clamped to the visible model — a move past an edge is a no-op (and does not emitactivecell-change). - Cell-level interaction.
Enter(orF2) focuses the active cell's first interactive control;Tab/Shift+Tabthen cycle within the cell (focus containment);Escapereturns focus to the cell and resumes navigation. Keys are only intercepted while a cell is focused — a caret inside an in-cell<input>reached withoutEnterkeeps its native behavior. - Mouse + roving model stay in sync. Clicking a cell makes it the active cell (the roving
tabindex="0"follows), so the next arrow key continues from where you clicked. - Index-addressed, sort/filter-stable. The active cell is tracked as a
{ rowIndex, colIndex }pair over the visible model — never a stored DOM node — so it survives a re-sort, filter, page change, or column hide/reorder/pin (it clamps to the new bounds rather than getting lost). Hidden columns are simply absent from the navigable order.
Cell range selection. In grid mode a rectangular cell-range selection extends via Shift+Arrow / Shift+Click, surfaced through the one-way range-change event (payload { anchor, focus }, each corner a { rowIndex, colIndex } pair, or { anchor: null, focus: null }) and the getSelectedRange() verb. It is never a model:true slice.
The APG grid pattern's selection keys work from any body cell:
Shift+Spacetoggles the row containing the active cell in the row-selection model (the same path as the row checkbox, sor-model:rowSelectionand its change event behave identically). A no-op whenselectionModeis'none'.Ctrl/Cmd+Spaceselects the column containing the active cell, as a full-height cell range — the same range copy, cut, clear and the fill handle act on.Escapein navigation mode collapses the current cell range. It only callspreventDefaultwhen there is a range to clear, so your ownEscapehandling is untouched otherwise.
Drive and observe it imperatively via the focusCell / getActiveCell / clearActiveCell handle verbs and the activecell-change event. The exact behavioral contract is locked by a cross-framework visual-regression matrix proving the same behavior on all six targets.
Clipboard, fill & clear
Grid mode also ships a spreadsheet-style clipboard, fill, and clear surface, scoped to the current active cell / range selection. Copy, paste, cut, and clear are no-ops — falling through to the browser's native behavior — while a header cell is active, so none of them ever silently mutates a body cell from an unexpected focus position. Select-all behaves differently (it always intercepts the key; see its bullet below):
- Copy —
Ctrl/Cmd+Cserializes the active range (or the single active cell when no range is set) to the system clipboard as TSV (tab-separated cells, newline-separated rows). - Paste —
Ctrl/Cmd+Vreads TSV off the clipboard and tiles it across the current range, anchored at its top-left corner: a clipboard block smaller than the destination range repeats (tiles) to fill the whole range; a clipboard block larger than the range pastes its full block, extending past the original selection (clamped only to the table's own row/column bounds). Each destination cell is written only if its column iseditableand the value passes that column's validator; everything else is silently skipped. The whole paste is oner-model:datawrite plus onecell-edit-commitper cell actually written, and an aria-live region announces how many of the attempted cells were committed. - Cut —
Ctrl/Cmd+Xcopies the range exactly as above, then clears the source cells through the same write path Clear (below) uses. Also reachable imperatively via thecut()handle verb. - Fill handle — dragging the small handle at the active range's bottom-right corner tiles the range's existing values across the cells the drag covers (pure value-copy — there is no series/pattern detection, unlike a spreadsheet's numeric-sequence fill).
- Clear —
Delete/Backspaceclears the active cell or range through the same validator-gated write funnel as paste, minus the clipboard copy. Non-editable and validator-rejected cells are left untouched. - Select all cells —
Ctrl/Cmd+Aselects the entire body as one rectangular range (always callspreventDefault, so the browser never selects the page itself while in grid mode). A no-op — selects nothing — when the active cell is a header.
Every mutation above (paste, fill, cut, clear) is reversible with Ctrl+Z once undoable is set — see Undo & redo below.
Undo & redo
Set undoable and every committed data mutation — a cell/row edit, a paste, a fill, a cut, or a clear — becomes one undo step:
Ctrl/Cmd+Zundoes the most recent mutation;Ctrl/Cmd+YorCtrl/Cmd+Shift+Zredoes it. Both work whether a header or a body cell is active — unlike the clipboard shortcuts above, undo/redo is grid-wide.undoLimit(default100) bounds how many snapshots are retained; the oldest is evicted once the stack exceeds it.history-changefires{ canUndo, canRedo }whenever that availability changes — drive an undo/redo toolbar button'sdisabledstate from it.- Five handle verbs mirror the keyboard:
undo(),redo(),canUndo(),canRedo(),clearHistory(). See the API reference for their exact contracts. Swapping in a newdataarray from outside the table clears history automatically — a fresh dataset never inherits the previous one's undo stack.
With undoable left at its default false, nothing is recorded, Ctrl+Z/Y are inert, and the grid is byte-behaviorally identical to a pre-undo build.
Accessibility
- Semantic ARIA table roles throughout:
role="table"/role="rowgroup"/role="row"/role="columnheader"/role="cell", witharia-sort(the string-safe'ascending'|'descending'|'none') on sortable headers. - Row counting. The
<table>root carriesaria-rowcount— the header row count plus the full filtered pre-pagination data total (not just the current page) — and every header/body<tr>carries a 1-basedaria-rowindexthat already accounts for the header rows ahead of it. These are present in bothtableandgridinteraction mode, and are unaffected by pagination or row windowing — the index always reflects the row's position in the full model. - Group-header rows (grouping active). A group-header row's
<tr>carriesaria-expanded("true"/"false"), reflecting that group's collapsed/expanded state. While grouping is active, every row — group header and leaf alike — additionally carriesaria-level(1-based nesting depth), giving the multi-level grouped-row hierarchy the same treegrid semantics APG expects. - Plain expandable rows.
<tr>-levelaria-expandedis reserved for group-header rows; a non-grouped expandable row'saria-expandedlives on its expander<button>instead, never on the row itself. The two cases never overlap — a group-header row's own expand affordance is a separate group-toggle button, not the leading detail chevron. - Column position is not yet advertised.
aria-colcount/aria-colindexare not emitted anywhere in the component. This means a column-windowed grid (virtual="columns"or"both") gives assistive tech no way to tell a windowed leaf column's true position among the full column set — only the row axis currently carries that information. - Every interactive control is a native, focusable element with an accessible name — the sort buttons, the select-all + per-row checkboxes, the pagination prev/next + page-size
<select>, the global + per-column filter inputs, the column-visibility<details>disclosure, the per-header pin buttons, and the edge resize handles. There is no div-with-click-only control. - The keyboard / focus surface is the table-oriented default (Tab between the native controls). Opt into
interactionMode="grid"(above) for the full WAI-ARIA grid pattern —role="grid", a roving single tab-stop, and 2-D APG arrow-key cell navigation — on top of the same accessible chrome. - Select-all scopes to the filtered rows (the TanStack default) and shows the indeterminate state on a partial selection.
Per-framework code
The per-target consumption snippet is on the usage page.
See also
- Editing — grid mode pairs naturally with cell editing.
- API reference — the
interactionModeprop, theactivecell-change/range-changeevents, and the grid-mode verbs.