Documenting Keyboard Contracts for Components

Permalink to "Documenting Keyboard Contracts for Components"

A keyboard contract is the specification of how a component behaves for keyboard and screen reader users: which keys do what, where focus goes, and what is announced. For simple controls, the ARIA Authoring Practices describe it. For data components — grids with editing, selection, sorting, row actions — the behaviour is richer and specific to your design system. If it is not written down, each product learns it by trial, help dialogs drift out of date, and regressions go unnoticed. Written as data, the contract becomes documentation, help text and tests at once.

This guide belongs to design system tokens & component contracts.

Spec reference

Permalink to "Spec reference"
  • ARIA Authoring Practices — baseline keyboard interaction for patterns (grid, treegrid, listbox, menu, tabs).
  • SC 2.1.1 (A) — functionality via keyboard; SC 2.1.4 (A) — character key shortcuts must be adjustable or scoped.
  • SC 3.3.2 (A) — instructions for non-standard keys.
  • aria-keyshortcuts — documents shortcuts on elements for assistive technology.
A grid keyboard contract organised by context Hierarchy: the grid contract with navigation keys, selection keys, editing keys and command shortcuts, each listing keys with actions. A grid keyboard contract organised by contextGrid keyboard contractversion 4.2Navigationarrows, Home/End, Ctrl+Home/End, Page Up/DownSelectionSpace, Shift+Space, Ctrl+A, Shift+arrowsEditingEnter or F2, Escape, Tab while editingCommandsShift+F10 menu; Ctrl+C copy; ? help
Contexts keep contracts readable and testable.

When to use — and when not to

Permalink to "When to use — and when not to"

Write contracts for every composite component in a design system: grids, trees, menus, tab sets, comboboxes, date pickers, charts with keyboard navigation. Keep them in the same repository as the component.

Do not write contracts that differ from the ARIA patterns without a reason; document deviations explicitly. Do not let help dialogs be written separately from contracts — they drift.

The misapplication: a wiki page describing grid keys, last updated two major versions ago, contradicting the component’s actual behaviour.

Wiki prose versus contract as data Comparison: prose on a wiki drifts and cannot be tested; a contract stored as data renders documentation and help dialogs and generates tests, so it stays true. Wiki prose versus contract as data✗ Wiki proseDrifts from the codeHelp dialog written separatelyNot testableNo versioningDeviations from APG undocumented✓ Contract as dataLives with the componentHelp dialog generated from contractTests generated per entryVersioned with the componentDeviations flagged in the data

Annotated code example

Permalink to "Annotated code example"
// grid.keyboard.js — the contract as data
export const gridContract = {
  version: '4.2',
  contexts: {
    navigation: [
      { keys: ['ArrowRight'], action: 'Move focus to next cell', focus: 'next cell', announce: 'cell content; header on column change' },
      { keys: ['Control+Home'], action: 'First cell of grid', focus: 'first cell', announce: 'cell content' },
      { keys: ['Shift+F10'], action: 'Open row menu', focus: 'first menu item', announce: 'menu name and item' },
    ],
    editing: [
      { keys: ['Enter', 'F2'], action: 'Edit cell', focus: 'editor', announce: 'edit field with value' },
      { keys: ['Escape'], action: 'Cancel edit', focus: 'cell', announce: 'original value' },
    ],
  },
  deviations: [{ keys: ['Tab'], note: 'Moves between cells while editing (APG: leaves grid). Escape then Tab leaves.' }],
};
// Tests generated from the contract (Playwright)
for (const [ctx, entries] of Object.entries(gridContract.contexts)) {
  for (const e of entries.filter(x => x.test)) {
    test(`${ctx}: ${e.keys[0]} — ${e.action}`, async ({ page }) => {
      await e.test.setup(page);
      await page.keyboard.press(e.keys[0]);
      await e.test.expect(page);                                    // focus and state assertions
    });
  }
}
// Help dialog rendered from the same data
<dl>{gridContract.contexts.navigation.map(e => <><dt><kbd>{e.keys.join(' or ')}</kbd></dt><dd>{e.action}</dd></>)}</dl>

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Contract field Why it matters
Keys What users press, including alternatives
Context When the key applies (navigation vs editing)
Action What happens
Focus Where focus ends up — testable
Announce What users should hear — checkable in manual tests
Deviation Where the component differs from APG and why
One contract, three outputs Chain: the contract data feeds component docs, the in-app keyboard help dialog and generated keyboard tests; a change to the contract updates all three. One contract, three outputsContract datakeys, actions,focus, announcementsDocsStorybook and docssite tablesHelp dialogin-app "?" dialogTestsPlaywright keyboardtestsrenderrendergenerate
Single source of truth for keyboard behaviour.

Integration context

Permalink to "Integration context"

In-app help built from contracts follows keyboard shortcut help dialogs. Shortcut conflicts are covered in avoiding shortcut conflicts with screen readers. Generated tests complement testing grid keyboard navigation with Playwright.

Gotchas

Permalink to "Gotchas"

Platform differences. Ctrl on Windows and Linux is often Cmd on macOS; store both or a platform-neutral notation and render per platform.

Screen reader modes. Some keys behave differently in browse mode; contracts describe focus-mode behaviour inside composites — note that.

Over-documentation. Do not list every browser default; focus on what the component handles.

Worked example

Permalink to "Worked example"

A grid component’s documentation listed 18 shortcuts on a wiki page; the help dialog in products listed 12, and the component handled 21. A user reported that Ctrl+Enter, documented as “save row”, did nothing. The team moved the contract into a data file beside the component, generated the docs table, the help dialog and Playwright tests from it, and added a CI check that every handled key appears in the contract. The next release removed two undocumented shortcuts and fixed the Ctrl+Enter discrepancy.

Common mistakes

Permalink to "Common mistakes"
  • Contracts written once and never updated.
  • Help dialogs hand-written separately from the contract.
  • No platform-specific key rendering (Ctrl versus Cmd).
  • Deviations from the ARIA patterns hidden rather than documented.

Adoption checklist

Permalink to "Adoption checklist"
  1. Start with the grid: write its contract as data from current behaviour.
  2. Generate the docs table and help dialog from it.
  3. Add a CI check that every key the component handles appears in the contract.
  4. Generate or link a test per contract entry.
  5. Repeat for trees, menus, comboboxes and charts.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Make the contract part of the component’s public API: changes to it are reviewed like prop changes, and breaking keyboard changes follow the same deprecation process.

FAQ

Permalink to "FAQ"
Should contracts include announcements?

Yes, as expected information rather than exact wording, so manual testers know what to listen for.

How do contracts handle user-customisable shortcuts?

Store defaults in the contract and render the user’s current mapping in help dialogs.

Do I need contracts for simple buttons?

No. Native controls follow platform behaviour; contracts are for composites and custom shortcuts.

How do contracts help screen reader users directly?

They feed help dialogs and aria-keyshortcuts, making keys discoverable.

How do I verify announcements?

Manually or with speech logs; contracts tell testers what to expect.

Can contracts be shared across frameworks?

Yes — store them as JSON in a shared package and render them in each framework’s docs.

Where should deviations be shown to users?

In the help dialog and the component’s description where they affect basic movement (such as Tab behaviour in entry mode).

How detailed should focus descriptions be?

Specific enough to test: “first menu item”, “same cell”, “row header of the new row”. Vague descriptions cannot be asserted.

Should contracts cover touch and switch input?

Mention pointer and touch equivalents for keyboard-only commands where they exist (menus, long-press), so the contract describes how every user reaches each function.

Permalink to "Related"

← Back to Design System Tokens & Component Contracts