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:

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:

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: