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.
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.
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 |
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"- Start with the grid: write its contract as data from current behaviour.
- Generate the docs table and help dialog from it.
- Add a CI check that every key the component handles appears in the contract.
- Generate or link a test per contract entry.
- 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.
Related
Permalink to "Related"- Keyboard shortcut help dialogs — presenting contracts
- Accessibility props for grid components — the other half of the API
- Testing grid keyboard navigation — tests