Lesson 5 of 8 · 55 min
Accessibility and design-system stewardship
Treat accessibility as architecture: WCAG 2.2 criteria FE owns, keyboard-first design, semantic HTML before ARIA, combobox/dialog patterns, and design systems as products with tokens, deprecation, and gates.
A11y is architecture
Semantic HTML before ARIA
1// Smell: div button2<div>Save</div> // no keyboard, no role, no form semantics3// Fix4Save5// Smell: aria on everything6<div> // reimplementing 7// When ARIA is justified: combobox, grid, tree — follow APG patterns exactly</div>Common mistake
“Adding role and aria-label to a div makes it accessible.”
Keyboard-first design
- 01Tab order matches visual order; no positive tabindex mess.
- 02Roving tabindex in toolbars/lists: one tab stop, arrows move focus.
- 03Focus trap in modal dialogs; restore focus to opener on close.
- 04Escape closes overlays; Enter/Space activate buttons.
- 05Skip link to main content on marketing/app shells.
- 06Visible focus — never outline: none without a strong replacement (Focus Appearance).
WCAG 2.2 criteria FE owns (interview shortlist)
- 012.4.11 Focus Not Obscured (Minimum, AA) — focused item not hidden under sticky chrome.
- 022.4.12 / 2.4.13 Focus Appearance — visible, sufficient area/contrast for focus indicator.
- 032.5.7 Dragging Movements — provide single-pointer alternative to drag-only UI.
- 042.5.8 Target Size Minimum (AA) — 24×24 CSS pixels (with exceptions).
- 053.2.6 Consistent Help — help mechanisms in consistent order.
- 063.3.7 Redundant Entry — do not re-ask info already given in the same process.
- 073.3.8 / 3.3.9 Accessible Authentication — no cognitive function tests as sole auth; allow paste/password managers.
Demo: custom dropdown → combobox pattern
1// Structural sketch (prefer Radix/React Aria in product code)2<div>3 Country4 5 <ul id="list">6 {items.map(i => (7 <li id="{i.id}">{i.label}</li>8 ))}9 </ul>10</div>Key idea
Dialog with focus trap (code altitude)
1// Product code: use DS Dialog. Interview sketch:2// on open: store document.activeElement; focus first focusable3// Tab cycles first↔last; Escape closes; on close: restore focus4// role="dialog" aria-modal="true" aria-labelledby=titleId5// Portal to document.body to escape overflow/stacking contextsScreen readers, live regions, motion
Design system as a product
- 01Tokens — primitive → semantic; no raw hex in features. Encode --focus-ring and --tap-target-min.
- 02Composition — small primitives > mega components with 40 booleans.
- 03Deprecation — timeline, codemods if possible, lint bans on old imports.
- 04Storybook — docs, controls, a11y addon, visual regression (Chromatic/Playwright).
- 05Gates — axe in CI on stories; contrast checks; forbidden CSS patterns.
- 06Versioning — semver for the package; breaking changes rare and loud (RFC).
- 07References — Primer, Polaris, Spectrum, Radix, shadcn for open patterns.
1:root {2 --color-red-500: #ef4444; /* primitive */3 --color-danger: var(--color-red-500); /* semantic */4 --space-2: 0.5rem;5 --focus-ring: 0 0 0 3px Highlight;6 --tap-target-min: 24px;7}8.alertDanger { color: var(--color-danger); }9// Deprecation: export old Button with JSDoc @deprecated + eslint banCommon mistake
“Design system is just a Storybook of buttons.”
Working with design and PM
Testing strategy for a11y
- 01Automated: axe-core / eslint-plugin-jsx-a11y — catch ~30–40%, not enough alone.
- 02Component tests: Testing Library with role queries (getByRole) — forces semantics.
- 03E2E: keyboard path for critical journeys in Playwright.
- 04Manual: SR pass on new patterns; zoom 200%; Windows high contrast spot check.
- 05Storybook a11y addon on every primitive story before merge.
Key idea
Failure modes (a11y production)
- 01Dialog without focus trap and without focus restoration.
- 02Icon-only button with missing or generic aria-label.
- 03Color contrast below 4.5:1 for body text.
- 04Form errors not linked via aria-describedby + aria-invalid.
- 05Live region spam on every stream token.
- 06Touch targets under 24×24 CSS px.
- 07Cognitive CAPTCHA as only auth path (3.3.8 risk).
Interview answer bank — a11y & design systems
- 01Q: First rule of ARIA? Do not use ARIA if a native HTML element already provides the semantics and keyboard behaviour. Wrong ARIA is worse than no ARIA because assistive tech trusts the roles you declare. Prefer button, a, input, select, dialog patterns from the platform, then APG for composite widgets.
- 02Q: How do you implement a modal correctly? Use role=dialog, aria-modal, labelled title, focus trap while open, Escape to close, restore focus to the opener on close, and portal to body to escape overflow. Prefer the design-system Dialog so every team inherits the contract. Interview freestyle should still name trap + restore even if code is sketched.
- 03Q: What is roving tabindex? A composite widget (toolbar, listbox options, tablist) exposes one tab stop to the page; arrow keys move focus among items by toggling tabindex 0/-1. This keeps tab order sane while enabling efficient keyboard navigation inside the widget. APG documents the pattern per widget type.
- 04Q: How do design tokens prevent a11y regressions? Encode focus ring, minimum target size, and contrast-safe semantic colours as tokens. Feature teams then inherit compliance instead of inventing one-off CSS that fails contrast or removes outlines. Lint against raw hex and outline:none without replacement.
- 05Q: What does accessible authentication (WCAG 2.2) change for FE? Do not require cognitive function tests (puzzles) as the only login path. Allow paste into password fields and support password managers. Prefer WebAuthn/passkeys and standard 2FA over memory puzzles. This is a product + FE joint ownership issue, not “just backend.”
1// Testing Library forces semantics2// Prefer:3screen.getByRole('button', { name: /save/i })4screen.getByRole('combobox', { name: /country/i })5// Avoid:6container.querySelector('.btn-primary')78// CI gate sketch9// 1. eslint-plugin-jsx-a11y on all TSX10// 2. axe on Storybook stories for primitives11// 3. Playwright keyboard path for checkout / auth12// 4. Manual SR pass when introducing a new APG patternAPG patterns you must name cold
- 01Combobox — input + popup listbox; arrows; Enter select; Escape close; aria-expanded/controls/activedescendant.
- 02Dialog — trap focus; Escape; restore opener; aria-modal; labelled by title.
- 03Tabs — tablist/tab/tabpanel; arrows among tabs; Tab moves into panel.
- 04Menu — menuitem roles; arrows; Escape; careful not to use menu for site nav (use navigation landmarks).
- 05Disclosure — button aria-expanded controls the region; native details/summary often enough.
1// Focus restore sketch (pair with DS Dialog in product)2const prev = useRef(null)3useEffect(() => {4 if (!open) return5 prev.current = document.activeElement as HTMLElement6 firstFocusableRef.current?.focus()7 return () => prev.current?.focus()8}, [open])Checkpoint
A custom div with onClick and aria-label is used as a primary action. What is the architectural a11y failure?
Checkpoint
When is ARIA a smell?
Checkpoint
WCAG 2.2 Target Size Minimum (AA) most directly requires…
Checkpoint
You own a design system. A team wants a one-off modal that traps focus incorrectly. Best stewardship move?
Checkpoint
Best automated + human testing combo for a new combobox?
Can you name WCAG 2.2 FE-owned criteria and defend semantic HTML vs ARIA in a design-system context?
Takeaways
- Semantic HTML first; ARIA only with APG-level correctness.
- Keyboard, focus visibility, and target size are architectural.
- WCAG 2.2 added FE-owned criteria — name them in senior rounds.
- Design system is a product: tokens, deprecation, Storybook, gates.
- Automate what you can; always manual keyboard/SR for new patterns.
Next: FE system design I — chat and realtime dashboards with full RADIO skeletons.
Sources
Free to read · better with Enzo
Learn it with Enzo
Save your progress, answer the checkpoints, and let Enzo quiz you on what you just read.