Accessibility Guide
Keyboard interaction, focus management, ARIA patterns, and testing checklists for building with GHDS.
This is reference documentation for building accessible screens with GHDS — see About & Accessibility for the project’s overall accessibility commitment and how the automated color-contrast validation works.
Keyboard Interaction
Most components (Button, Card, Input, Alert, Badge, Avatar, Spinner, Progress,
Skeleton, Table) rely entirely on native browser keyboard handling — no custom onKeyDown or
tabIndex logic. Overlay components add their own bindings for dismissal and navigation.
| Key | Action |
|---|---|
Tab | Move focus to the next focusable element in document order. |
Shift + Tab | Move focus to the previous focusable element. |
Enter / Space | Activate a focused Button. |
Escape | Close an open Modal (dismisses the dialog and restores focus to the trigger). Also dismisses Tooltip and Menu. |
↑ ↓ ← → | Navigate items within an open Menu (arrow keys), or cycle through Tabs (left/right arrows). |
Focus Management
- Visible focus:
ButtonandInputeach track their ownfocusedstate and render a visible ring using acomp.*.focus.ringorborder-focustoken — never rely on removing the outline without replacing it. - Tab order: a native HTML
disabledattribute (used byButton’s default render path) removes the element from the tab order entirely.Button’sasChildpath only setsaria-disabled, which does not remove it from the tab order — if you disable anasChildbutton, you must also prevent activation yourself. - Focus trap / restore:
Modalimplements full focus-trap+restore on React and Web Components. On open, focus moves to the first focusable element inside the dialog (or the dialog itself); Tab/Shift+Tab cycle within; Escape or scrim-click dismiss and restore focus to the element that opened the dialog. React Native delegates focus containment toaccessibilityViewIsModal.Menutraps focus similarly while open.ToastandTooltipdo not trap focus — they are transient / supplementary.
ARIA Pattern Reference
| Component | Pattern |
|---|---|
Button | Native <button> role (or aria-disabled on the asChild path — see Keyboard Interaction above). Decorative sketch outline is aria-hidden="true". |
Card | No role by default on React — the caller supplies one. Web Components/React Native automatically expose role="group"/accessibilityRole="summary" when a label is given. |
Input | aria-invalid + aria-describedby (pointing at a role="alert" error message) on React when error is set. Label association via <label for>/Radix Label. |
Modal | role="dialog" + aria-modal="true", title wired via aria-labelledby. React portals to document.body; WC uses native <dialog> + ::backdrop; RN uses accessibilityViewIsModal. |
Alert | role="status" by default, role="alert" for danger variant (assertive). Severity-coloured icon with accessible name from the heading text. |
Toast | role="status" (polite) for info/success/warning; role="alert" (assertive) for danger. Auto-dismiss timer; close button labelled “Dismiss”. |
Tooltip | role="tooltip" with the trigger linked via aria-describedby. Web: hover (after delay) or focus; dismiss on Escape/blur/leave. RN: tap-to-toggle with accessibilityHint on trigger. |
Menu | role="menu" with role="menuitem" on items. aria-activedescendant (WC) manages active descendant; React uses roving tabindex. Arrow-key navigation, Escape dismiss. |
Screen Reader Testing Checklist
- Every interactive element has an accessible name — either visible text, a
label, or an explicitaria-label/accessibilityLabel. This includes buttons insideModal,Toast, andAlert(the close/dismiss buttons). - Disabling a component removes it from both the tab order and screen reader interaction — verify both, since (per Focus Management above) they don’t always happen together.
- An
Input’s error message is announced when it appears, not just visually shown.FormFieldautomates this witharia-describedbypointing at the error’srole="alert". - Opening a
Modalmoves focus into the dialog and traps it; closing restores focus to the trigger. This applies to both keyboard (Tab/Shift+Tab) and screen-reader virtual cursor. -
ToastandAlertwithvariant="danger"are announced assertively (role="alert"); other severities use a politerole="status"that does not interrupt. - Decorative elements (the sketch outline SVGs) are silent — confirm they don’t get announced or receive focus.
- Toggling dark mode doesn’t change any of the above (the
[data-theme]switch is purely visual).
How Color Contrast Validation Works
Every sys.color.* text/background pairing is checked automatically at build time in
@ghds/tokens — the check computes the real WCAG relative-luminance contrast ratio for every
text/icon/border role against every bg surface, plus explicit text-on-filled-background
pairs, and asserts 4.5:1 for normal text or 3:1 for large text/icons/borders. A small, documented
set of pairs (disabled text/background, decorative borders) is exempt.
What this means for you: you get the WCAG 2.1 AA guarantee for free as long as you consume
sys.color.* (or comp.* tokens that alias them) — the guarantee does not extend to any custom
color you introduce outside the token set, or to the exempt pairs above.