Accessibility

SvGrid implements the WAI-ARIA 1.2 grid pattern with full keyboard navigation and a screen-reader announcement layer. This page documents exactly what the grid does, where the responsibility line sits between the library and your code, and how to verify conformance.

An ARIA grid whose rows carry aria-rowindex and cells carry aria-colindex, with the active cell that arrow keys move named by aria-activedescendant, and an aria-live region for status messages.

Live demo - high-contrast toggle, aria-live log, and a focus-trap walk-through:

Open the live example: Accessibility (Keyboard & Accessibility)

TL;DR

Standard Status
WAI-ARIA 1.2 grid pattern Implemented (see Roles & properties).
WCAG 2.2 AA Self-assessed, criterion by criterion, in the Accessibility Conformance Report, gaps included. Structure is audited by axe-core in CI, in jsdom and in a real browser. Contrast is enforced in CI for all 20 built-in presets, light and dark; a custom theme is yours to verify.
Keyboard navigation Navigation, selection, sorting, filtering and editing; see Keyboard map. Row dragging and moving columns by drag have no keyboard path yet; the ACR lists them.
Screen-reader announcements Filter results and bulk selection changes, via a single aria-live=polite region. See What the grid announces.
Reduced motion The cell flash and the loading shimmer stop under prefers-reduced-motion: reduce.
Forced colors / high contrast The active cell switches to a system Highlight outline (a box-shadow ring would vanish). Tested in a browser with forced colors emulated.

Roles & properties

Element Role Notable attributes
<table> root grid (treegrid with treeData or server grouping) tabindex="0", aria-activedescendant pointing at the active cell, aria-rowcount, aria-colcount
<thead>, <tbody>, <tfoot> rowgroup
Body <tr> row aria-rowindex (1-based, absolute, so it holds under virtualization); aria-level / aria-expanded on tree and group rows
Header <th> columnheader aria-sort ("ascending" / "descending" / "none") on sortable columns
Body <td> gridcell id, aria-colindex, aria-rowindex, aria-selected (row selection); aria-invalid plus the message when validate rejects the value
Selection checkboxes checkbox aria-checked, "mixed" on the header when some rows are selected
Filter popover menu
Cell editor input textbox (text, number and multi-line editors) aria-label is the column header
Live announcement region status (aria-live="polite" on a visually-hidden <div>)

Header rows carry no aria-rowindex yet, and a cell range selection (as opposed to row selection) is not exposed through aria-selected. Both are listed in the ACR.

The grid never sets role="presentation" on table elements - screen readers receive a fully-structured grid.

Keyboard map

Standard ARIA grid navigation, plus a handful of grid-specific shortcuts:

Key Action
Tab / Shift+Tab Move the active cell right / left, wrapping at row ends. On the last cell (first, for Shift+Tab) focus leaves the grid for the next (previous) element on the page. Ctrl+End then Tab, or Ctrl+Home then Shift+Tab, leaves from anywhere.
Arrow Up/Down Move active cell one row.
Arrow Left/Right Move active cell one column.
Home Active cell → first column in the row.
End Active cell → last column in the row.
Ctrl/Cmd + Home Active cell → top-left of the grid.
Ctrl/Cmd + End Active cell → bottom-right.
Ctrl/Cmd + Arrow Reserved. The grid does not move the active cell; enableSheet() in @svgrid/enterprise binds it to Excel's jump-to-edge-of-data-region. See Excel keyboard shortcuts.
Page Up / Page Down Scroll a viewport's worth of rows.

Selection (when rowSelectionFeature is on)

Key Action
Space Toggle the active row's selection.
Ctrl/Cmd + A Not bound by the grid; the browser's select-all applies. The header checkbox selects every row, and api.selectCells selects ranges.
Shift + Arrow keys Extend the cell range from the active cell.
Ctrl/Cmd + Click on a cell Add a cell range to the selection.

Sort + filter

Key Action
Enter / Space (on a header's sort button) Toggle sort: none → asc → desc → none.
Enter / Space (on a header's filter button) Open the filter menu.
Escape (in a filter menu) Close the menu.

Header buttons are reached with the mouse or, from outside the grid, with Tab; they are not part of the cell navigation.

Editing

Key Action
F2 / double-click Enter edit mode on the active cell. (Enter moves down, as in a spreadsheet.)
Type any character Start editing with that character as the first input.
Enter / Tab (while editing) Commit and move to the next cell / row.
Escape (while editing) Cancel; revert the cell.
Delete / Backspace Clear the active cell (for cells whose editor supports clearing).

Tree rows (when you build them)

The keyboard handler in Tree rows adds:

Key Action (active cell in name column)
Arrow Right Expand a collapsed node.
Arrow Left Collapse an expanded node.
Enter / Space Toggle.

On a grouped grid, a tree (getSubRows) or a server-side row model the grid handles these itself, and Ctrl+Enter on any cell of a group row toggles it; on the server model a leaf with a detail panel (renderDetailRow) opens or closes it with the same key.

Screen-reader announcements

The grid shares one visually-hidden aria-live="polite" region with the rest of the component library, and is deliberately sparing about what it puts there.

What the grid announces

Event What is said
A filter changes the matching rows "12 of 250 rows match the current filters", or "No rows match the current filters"
Every filter is cleared "Filters cleared, showing all 250 rows"
A bulk selection change "250 rows selected" / "Selection cleared"

Filter announcements are debounced by 400 ms, so typing in the search box announces the count you stopped on rather than one for every prefix. They are also skipped entirely under externalFilter, because there the server decides what matched and the local count would describe only the page in hand.

What it deliberately does not announce

This is the more important half. A live region is not the only way a screen reader learns something, and announcing what the accessibility tree already carries makes the grid talk over itself:

Translating them

Announcements are full sentences, so unlike the single-word labels elsewhere in localeText they take {placeholders} and you control the word order:

The examples on this page run against these rows:

<script lang="ts">
  import { SvGrid, type GridColumns, type SvGridApi } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000 },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000 },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000 },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000 },
    { id: 5, name: 'Barbara Liskov', department: 'Platform',    city: 'Boston',   age: 52, salary: 172000 },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 190 },
    { field: 'department', header: 'Department', width: 150 },
    { field: 'city',       header: 'City',       width: 130 },
    { field: 'age',        header: 'Age',        width: 80 },
    { field: 'salary',     header: 'Salary',     width: 130, format: { type: 'currency', currency: 'USD' } },
  ]
</script>
<SvGrid
  localization={{
    text: {
      announceFilterResults: '{visible} van de {total} rijen komen overeen',
      announceNoMatches: 'Geen rijen komen overeen',
      announceFiltersCleared: 'Filters gewist, alle {total} rijen worden getoond',
      announceRowsSelected: '{count} rijen geselecteerd',
      announceSelectionCleared: 'Selectie gewist',
    },
  }}
/>

An unknown placeholder is left in the output rather than blanked, so a typo in an override is visible instead of silently eating the number.

Focus management

High-contrast / forced-colors mode

Under Windows High Contrast (forced-colors: active) the active cell gets a 2 px outline in the system Highlight colour, because forced colors removes the box-shadow ring the grid draws otherwise. tests/e2e/grid-a11y.spec.ts checks it with forced colors emulated in Chromium.

Right to left

Set dir="rtl" on the page or any element above the grid and the layout mirrors: the row-number gutter and every pinned column stick to the right edge and stay there while the cells scroll under them, because the pins are logical (inset-inline-start, inset-inline-end) rather than a physical left. Nothing to opt into and no second stylesheet.

Reduced motion

When prefers-reduced-motion: reduce matches:

Keyboard scrolling is always instant, so there is no smooth scroll to turn off. Chevron rotations still use a short transition. You don't need to opt in; both rules are plain CSS media queries.

What you're responsible for

The grid can't know:

How to verify

  1. Keyboard sweep. Unplug your mouse. Tab in, navigate every cell, sort, filter, edit, undo. If any action is unreachable, file an issue.

  2. NVDA + VoiceOver. Each makes different choices about announcement verbosity. Test both.

  3. Lighthouse accessibility audit. Run it against your own build. Most deltas are contrast issues in your theme rather than grid markup, so check those first.

    We run axe-core against a rendered grid in CI (a11y.axe.test.ts), covering the plain grid, the filter row, row selection and pagination. That suite runs in jsdom, which does no layout, so axe's own colour-contrast rule is disabled there.

    Contrast is covered separately and more thoroughly: themes/contrast.test.ts computes WCAG ratios for every built-in preset in both light and dark and fails the build below AA. Body text, secondary text, header text, text on zebra / hovered / selected rows, and text on accent-filled controls all have to clear 4.5:1, and the accent has to clear 3:1 where it signals focus or selection. So the presets are checked exhaustively rather than whichever one a browser test happened to load. What is still yours to verify is a custom theme.

  4. axe-core in your e2e suite.

    import AxeBuilder from '@axe-core/playwright'
    const results = await new AxeBuilder({ page })
      .include('.sv-grid-container')
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
      .analyze()
    expect(results.violations).toEqual([])
    

Frequently asked questions

Is SvGrid accessible / WCAG compliant?

SvGrid implements the WAI-ARIA 1.2 grid pattern: role="grid" structure, full keyboard navigation (arrows, Home/End, Page Up/Down, Ctrl+Home/End), a focus ring on the active cell, and an aria-live announcement layer. Final WCAG conformance also depends on your own cell content and color choices - this page documents exactly where that line sits.

Does SvGrid work with screen readers?

The grid exposes the ARIA grid roles and states, and emits aria-live announcements for filtering and bulk selection changes. Sort state is read from aria-sort on the header rather than announced. No scripted NVDA, JAWS or VoiceOver pass has been recorded yet; the ACR says so where it matters. Because it renders real DOM (not canvas), the content is also selectable and machine-readable.

How do I verify accessibility in my app?

Run axe-core against the rendered grid (recipe in the testing guide) and test keyboard-only navigation. Pair it with the high-contrast focus toggle and the --sg-* contrast tokens to meet your target contrast ratios.

More examples

Keyboard shortcuts + a11y

Ctrl+K command palette, Ctrl+/ cheat sheet, vim-style gg / G chord nav. Layers on top of the grid's WAI-ARIA grid pattern.

Open the live example: Keyboard shortcuts + a11y (Keyboard & Accessibility)

High-contrast theme

WCAG 2.2 AAA-grade preset for accessibility procurement. Token block opts a subtree into the high-contrast skin while the rest of the page stays standard. Light + dark.

Open the live example: High-contrast theme (Themes & Styling)

Try the keyboard

Everything below is keyboard-reachable. Tab once to enter the grid, then arrow between cells - the grid is a single tab stop, not one per cell, which is what makes a 10,000-row table usable without a mouse.

<SvGrid data={people} {columns} sortable filterable enableCellSelection editable />

Try: Tab in, arrows to move, Enter to edit, Escape to cancel, Shift+arrows to extend a range, Ctrl/Cmd+C to copy it.

What a screen reader is told

The grid root carries role="grid" with aria-rowcount and aria-colcount, headers carry aria-sort, and rows and cells carry their index. Sort a column in the example above and inspect the header: the aria-sort attribute flips with it, which is how a screen reader user knows the order changed at all.

<script lang="ts">
  let report = $state('(sort a column)')

  function inspect() {
    const grid = document.querySelector('[role="grid"], [role="treegrid"]')
    const sorted = document.querySelectorAll('[role="columnheader"][aria-sort]')
    report =
      'role=' + (grid?.getAttribute('role') ?? 'missing') +
      ' | aria-rowcount=' + (grid?.getAttribute('aria-rowcount') ?? 'missing') +
      ' | headers with aria-sort: ' + sorted.length
  }
</script>

<SvGrid data={people} {columns} sortable />

<button type="button" onclick={inspect}>Inspect the ARIA</button>
<p><code>{report}</code></p>

See also

Live examples

  • Accessibility - WAI-ARIA grid, keyboard navigation, aria-live announcements, focus toggle.
  • Keyboard shortcuts + a11y - Ctrl+K command palette, Ctrl+/ cheat sheet, vim-style gg / G chord nav. Layers on top of the grid's WAI-ARIA grid pattern + roving tabindex.
  • High-contrast theme - WCAG 2.2 AAA-grade preset for accessibility procurement. Token block opts a subtree into the high-contrast skin while the rest of the page stays standard. Light + dark.

Related articles

  • Choosing the Most Accessible Svelte Data Grid - A practical guide to testing data grid accessibility - ARIA roles, keyboard navigation, focus management under virtualization, and screen-reader behavior - so you can verify claims yourself.
  • Accessibility from the Ground Up - How SvGrid bakes WAI-ARIA roles and keyboard navigation into the core - not as a post-launch checkbox, but as a design constraint that shaped every feature.
  • What Makes a Data Table Accessible? (WCAG) - Semantic markup, WAI-ARIA grid roles, keyboard navigation, and the WCAG criteria that matter most for interactive data tables and grids.
  • Keyboard Navigation and Accessibility in SvGrid - WAI-ARIA grid semantics, roving tabindex, and live-region announcements are built into SvGrid from the start. Here is what that means in practice and where custom cells require your attention.