Skip to content

Accessibility

@toolbox-web/grid follows the WAI-ARIA Grid Pattern to provide an accessible data grid experience. This page documents the ARIA attributes, keyboard interactions, and best practices.

The grid applies the following ARIA roles and attributes automatically:

ElementRoleAttributes
<tbw-grid>gridaria-rowcount, aria-colcount, aria-multiselectable
Header containerrowgroup
Header rowrowaria-rowindex="1"
Header cellcolumnheaderaria-sort, aria-colindex
Body containerrowgroup
Data rowrowaria-rowindex, aria-selected, aria-expanded
Data cellgridcellaria-colindex, aria-selected, aria-readonly
AttributeApplied WhenValues
aria-sortColumn is sortedascending, descending, none
aria-selectedRow or cell is selectedtrue, false
aria-expandedRow grouping/tree is activetrue, false
aria-multiselectableSelection plugin allows multi-selecttrue
aria-readonlyCell is not editabletrue
aria-rowindexAlways (1-based)Row position in full dataset
aria-colindexAlways (1-based)Column position

The grid implements full keyboard navigation following the WAI-ARIA grid pattern:

KeyAction
/ Move focus between rows
/ Move focus between cells
HomeMove to first cell in row
EndMove to last cell in row
Ctrl + HomeMove to first cell in grid
Ctrl + EndMove to last cell in grid
PgUpScroll up one viewport
PgDnScroll down one viewport
⇥ TabMove to next cell (wraps to next row)
⇧ Shift + ⇥ TabMove to previous cell (wraps to previous row)

Keyboard shortcuts that depend on a plugin being installed are documented on each plugin’s own page — that’s the source of truth and stays in sync with the implementation:

  • SelectionSpace, Shift + arrows / PgUp / PgDn / Ctrl + Home/End, Ctrl + A, Esc
  • EditingEnter (start row edit / commit), F2 (single-cell edit), Tab / Shift + Tab, Esc (cancel)
  • ClipboardCtrl/Cmd + C / X / V
  • Context MenuShift + F10 or the dedicated ☰ Menu key opens the menu at the focused cell; / navigates, Enter/Space activates, Esc closes
  • Row GroupingSpace toggles expand/collapse on a group or tree node

The grid uses visible focus indicators that meet WCAG 2.1 Level AA requirements:

tbw-grid {
/* Customize focus ring */
--tbw-color-focus-ring: #2563eb;
}

The focus ring is 2px solid and uses outline (not border) so it doesn’t affect layout.

When editing a cell, focus is trapped within the editor until the user commits (Enter/Tab) or cancels (Escape). Overlay editors (date pickers, dropdowns) use registerExternalFocusContainer() to extend the focus trap.

The grid uses a roving tabindex pattern:

  • Only the currently focused cell has tabindex="0"
  • All other cells have tabindex="-1"
  • This means Tab moves focus out of the grid, and Shift+Tab moves focus back to the last focused cell

Provide accessible labels for screen readers:

<tbw-grid aria-label="Employee directory">

Or reference a visible heading:

<h2 id="grid-heading">Employees</h2>
<tbw-grid aria-labelledby="grid-heading">

The grid uses aria-live regions to announce dynamic changes to screen readers:

  • Sort changes (“Sorted by Name, ascending”)
  • Filter changes (“Filter applied on Name”, “All filters cleared”)
  • Group expand/collapse (“Group Engineering expanded, 5 rows”)
  • Selection changes (“3 rows selected”)
  • Editing lifecycle (“Editing row 1”, “Row 1 saved”)

You can disable or customize live region announcements via the a11y config — see A11yMessages for the full list of overridable messages and their signatures:

// Disable all announcements
grid.gridConfig = {
a11y: { announcements: false },
};
// Override messages for internationalization
grid.gridConfig = {
a11y: {
messages: {
sortApplied: (col, dir) => `Trié par ${col}, ${dir}`,
sortCleared: () => 'Tri effacé',
filterApplied: (col) => `Filtre appliqué sur ${col}`,
filterCleared: (col) => `Filtre effacé de ${col}`,
allFiltersCleared: () => 'Tous les filtres effacés',
groupExpanded: (name, count) => `Groupe ${name} développé, ${count} lignes`,
groupCollapsed: (name) => `Groupe ${name} réduit`,
selectionChanged: (count) => `${count} lignes sélectionnées`,
editingStarted: (rowIndex) => `Édition de la ligne ${rowIndex + 1}`,
editingCommitted: (rowIndex) => `Ligne ${rowIndex + 1} enregistrée`,
dataLoaded: (count) => `${count} lignes chargées`,
},
},
};

Only override the messages you need — defaults (English) are used for the rest.

Column headers include:

  • Column name via text content
  • Sort direction via aria-sort
  • Filter state via aria-description (“Filtered”)

The grid’s CSS variable system makes high contrast easy:

/* High contrast theme */
tbw-grid.high-contrast {
--tbw-color-bg: #000;
--tbw-color-fg: #fff;
--tbw-color-border: #fff;
--tbw-color-header-bg: #1a1a1a;
--tbw-color-row-hover: #333;
--tbw-color-focus-ring: #ffff00;
--tbw-color-accent: #00ffff;
}
import '@toolbox-web/grid/themes/dg-theme-contrast.css';

The grid ships with a built-in @media (forced-colors: active) block that remaps all theming variables to Windows system colors (CanvasText, Canvas, Highlight, HighlightText). Focus rings and selected rows are also overridden to use Highlight. No extra configuration needed — it works automatically.

CriterionLevelStatusNotes
1.1.1 Non-text ContentAIcons have text alternatives
1.3.1 Info and RelationshipsAARIA roles convey structure
1.3.2 Meaningful SequenceADOM order matches visual order
1.4.1 Use of ColorAStatus not conveyed by color alone
1.4.3 Contrast (Min)AADefault theme meets 4.5:1 ratio
1.4.11 Non-text ContrastAAFocus indicators visible at 3:1
2.1.1 KeyboardAAll functions accessible via keyboard
2.1.2 No Keyboard TrapATab exits the grid; Escape exits editors
2.4.3 Focus OrderALogical focus order follows grid structure
2.4.7 Focus VisibleAAClear focus indicators
4.1.2 Name, Role, ValueAARIA attributes on all interactive elements

The grid handles most accessibility concerns out of the box:

  • ARIA roles & attributesrole="grid", role="row", role="gridcell", aria-rowcount, aria-colcount, aria-rowindex, aria-colindex are always set
  • Keyboard navigation — Arrow keys, Tab, Enter, Home/End, PgUp/PgDn all work by default (core feature, not a plugin)
  • Column headers — Always rendered with role="columnheader"; cannot be hidden
  • Column type inference — Types (number, date, boolean) are auto-detected from the first data row
  • Grid label — Auto-derived from the shell title (<tbw-grid-header title="..."> or shell.header.title config)
  • Windows High Contrastforced-colors media query is built into core CSS
  • Live announcements — Sort, filter, selection, grouping, and editing changes are announced via aria-live regions (configurable via a11y config)
  • Plugin ARIA — Selection, editing, filtering, tree, and grouping plugins manage their own ARIA attributes (aria-selected, aria-readonly, aria-expanded, aria-multiselectable, aria-description)

These require explicit action:

  1. Provide a grid label when not using a shell — Without <tbw-grid-header title="...">, set gridAriaLabel in config or add aria-label on the element
  2. Test with screen readers — NVDA (Windows), VoiceOver (macOS), Orca (Linux)
  3. Don’t override keyboard handling — The grid follows WAI-ARIA patterns; custom key listeners may conflict
  4. Use the contrast theme for additional a11y — Import dg-theme-contrast.css for higher contrast beyond the built-in forced-colors support
AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://raw.githubusercontent.com/OysteinAmundsen/toolbox/main/llms-full.txt