Accessibility Plugin (WCAG 2.1 AA)
@gridstorm/plugin-a11y adds a complete WCAG 2.1 AA compliance layer on top of the DOM renderer's existing ARIA attributes. It provides live screen reader announcements, skip navigation links, high-contrast CSS, and keyboard enhancements for column headers.
:::tip This plugin is a legal differentiator — WCAG 2.1 AA compliance is now a contractual requirement in government and healthcare procurement. ADA lawsuits average $20K–$50K to settle. :::
Installation
npm install @gridstorm/plugin-a11y
Quick Start
import { createGrid } from '@gridstorm/core';
import { A11yPlugin } from '@gridstorm/plugin-a11y';
const grid = createGrid({
columns: [/* ... */],
rowData: [/* ... */],
plugins: [
A11yPlugin({
announcements: true,
skipNav: true,
highContrast: true,
}),
],
});
What it adds
The GridStorm DOM renderer already provides the structural ARIA foundation:
role="grid",role="row",role="gridcell",role="columnheader"aria-rowcount,aria-colcount,aria-sort,aria-rowindex,aria-colindexaria-selected,aria-expanded,aria-readonly
This plugin adds the behavioral accessibility layer:
| Feature | Description |
|---|---|
| Screen reader announcements | Sort changes, filter changes, selection counts, cell editing, pagination, group expand/collapse |
| Skip navigation | Two visually-hidden links: "Skip to grid content" and "Skip past grid" |
| High-contrast CSS | @media (prefers-contrast: more) and @media (forced-colors: active) support |
| Header keyboard | Enter to sort column headers, tabindex management |
| Focus mode tracking | Tracks navigate vs edit mode for correct keyboard behaviour |
Options
interface A11yPluginOptions {
/** Enable screen reader announcements via aria-live region. Default: true */
announcements?: boolean;
/** Inject skip navigation links before the grid. Default: true */
skipNav?: boolean;
/** Inject high-contrast CSS media queries. Default: true */
highContrast?: boolean;
/** Debounce announcement delay in ms. Default: 150 */
announceDebounce?: number;
/** Politeness level for announcements. Default: 'polite' */
politeness?: 'polite' | 'assertive';
/** Custom announcement formatter — return null to suppress */
formatAnnouncement?: (type: AnnouncementType, context: AnnouncementContext) => string | null;
}
Announcement Types
The plugin listens to these grid events and generates plain-language announcements:
| Event | Example Announcement |
|---|---|
column:sort:changed |
"Sorted by Name ascending" |
filter:changed |
"Filter applied on City" |
selection:changed |
"3 rows selected" |
cell:editingStarted |
"Editing cell in column Age, row 5" |
cell:editingStopped |
"Finished editing cell in column Age" |
row:groupOpened |
"Group Electronics expanded" |
pagination:changed |
"Page 2 of 10" |
rowData:changed |
"500 rows loaded" |
Custom Announcements
Override any announcement with your own text:
A11yPlugin({
formatAnnouncement(type, ctx) {
if (type === 'selection-changed') {
return ctx.count === 0
? 'No rows selected'
: `${ctx.count} ${ctx.count === 1 ? 'record' : 'records'} selected`;
}
return null; // use default for everything else
},
})
Commands
Dispatch manually from your application code:
// Manual screen reader announcement
grid.commandBus.dispatch('a11y:announce', { message: 'Export complete — 250 rows saved.' });
// Switch focus mode programmatically
grid.commandBus.dispatch('a11y:setMode', { mode: 'edit' });
// Toggle high contrast overlay
grid.commandBus.dispatch('a11y:toggleHighContrast', {});
Skip Navigation
Two visually-hidden links are injected immediately before the grid root:
- Skip to grid content — focuses the first
[role="gridcell"] - Skip past grid — focuses the next focusable element after the grid
Both links become visible (floating badge in the top-left corner) when focused via keyboard.
High-Contrast Support
The plugin injects a <style> element with two media queries:
@media (prefers-contrast: more) {
/* Solid borders, high-contrast focus rings, accessible selection colours */
}
@media (forced-colors: active) {
/* Windows High Contrast Mode — uses system colour keywords */
/* Uses ButtonText, Highlight, HighlightText instead of custom colours */
}
:::caution
Avoid using box-shadow for focus indicators — it disappears in forced-colors mode. The plugin enforces outline instead.
:::
Plugin State
Access the plugin's runtime state:
const state = grid.store.getState().pluginState['a11y'];
// {
// announcementsEnabled: boolean,
// highContrastActive: boolean,
// lastAnnouncement: string,
// focusMode: 'navigate' | 'edit',
// }
WCAG 2.1 AA Checklist
| Criterion | Status | How |
|---|---|---|
| 1.3.1 Info and Relationships | ✅ | role="grid/row/gridcell/columnheader" |
| 1.4.3 Contrast (Minimum) | ✅ | CSS tokens pass AA contrast ratios |
| 1.4.11 Non-text Contrast | ✅ | Focus rings ≥ 3:1 |
| 2.1.1 Keyboard | ✅ | Arrow keys, Tab, Enter, Escape |
| 2.1.2 No Keyboard Trap | ✅ | Skip-past-grid link |
| 2.4.1 Bypass Blocks | ✅ | Skip navigation links |
| 2.4.3 Focus Order | ✅ | Roving tabindex pattern |
| 2.4.7 Focus Visible | ✅ | Always-on focus ring |
| 3.2.2 On Input | ✅ | No unexpected context changes |
| 4.1.2 Name, Role, Value | ✅ | ARIA labels on all interactive elements |
| 4.1.3 Status Messages | ✅ | aria-live announcements |
Compatibility
Works with all major screen readers:
- NVDA (Windows) + Chrome/Firefox
- JAWS (Windows) + Chrome/Edge
- VoiceOver (macOS/iOS) + Safari
- TalkBack (Android) + Chrome