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

Accessibility bolted on at the end fails interviews and lawsuits. Senior FE designs keyboard paths, semantics, and focus as part of the component model — and runs the design system as a product with tokens, deprecation, and gates.
WCAG 2.2 (W3C Recommendation) added criteria that FE actually owns: focus not obscured, focus appearance, dragging alternatives, target size minimum (24×24 CSS px at AA), consistent help, redundant entry, accessible authentication. Name them when relevant — signal over “we use aria sometimes.”

Semantic HTML before ARIA

ARIA is a patch for missing semantics, not a badge of quality. Wrong ARIA is worse than none. Prefer button, a, input, label, nav, main, dialog (with correct focus). First rule of ARIA: do not use ARIA if a native element works.
tsx
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>

Keyboard-first design

  1. 01Tab order matches visual order; no positive tabindex mess.
  2. 02Roving tabindex in toolbars/lists: one tab stop, arrows move focus.
  3. 03Focus trap in modal dialogs; restore focus to opener on close.
  4. 04Escape closes overlays; Enter/Space activate buttons.
  5. 05Skip link to main content on marketing/app shells.
  6. 06Visible focus — never outline: none without a strong replacement (Focus Appearance).

WCAG 2.2 criteria FE owns (interview shortlist)

  1. 012.4.11 Focus Not Obscured (Minimum, AA) — focused item not hidden under sticky chrome.
  2. 022.4.12 / 2.4.13 Focus Appearance — visible, sufficient area/contrast for focus indicator.
  3. 032.5.7 Dragging Movements — provide single-pointer alternative to drag-only UI.
  4. 042.5.8 Target Size Minimum (AA) — 24×24 CSS pixels (with exceptions).
  5. 053.2.6 Consistent Help — help mechanisms in consistent order.
  6. 063.3.7 Redundant Entry — do not re-ask info already given in the same process.
  7. 073.3.8 / 3.3.9 Accessible Authentication — no cognitive function tests as sole auth; allow paste/password managers.
Also still core: colour contrast (4.5:1 body text), captions/transcripts when media, name/role/value for custom widgets, status messages via polite live regions without spam. Encode focus ring and min tap target into design tokens so product teams inherit compliance.

Demo: custom dropdown → combobox pattern

Interview machine-coding often includes a select-like widget. Do not freestyle. Follow WAI-ARIA Authoring Practices combobox/listbox: popup id, aria-expanded, aria-activedescendant or focus movement, aria-controls, keyboard (Escape, arrows, Enter, typeahead).
tsx
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>

Dialog with focus trap (code altitude)

text
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 contexts

Screen readers, live regions, motion

Test with VoiceOver/NVDA at least once per major pattern. aria-live="polite" for non-urgent status; assertive sparingly. Streaming AI UIs must not announce every token — announce completion or throttled status. Respect prefers-reduced-motion for decorative motion. Icon-only buttons need descriptive aria-label (not “button”).

Design system as a product

Staff-level FE signal: you do not only “use” the system — you steward it. Tokens (colour, space, type, elevation), semantic aliases (danger, surface), theming, contribution model, deprecation policy, and release notes. A DS steward exercises Architect + Tech Lead archetypes: sets the contract and the runway.
  1. 01Tokens — primitive → semantic; no raw hex in features. Encode --focus-ring and --tap-target-min.
  2. 02Composition — small primitives > mega components with 40 booleans.
  3. 03Deprecation — timeline, codemods if possible, lint bans on old imports.
  4. 04Storybook — docs, controls, a11y addon, visual regression (Chromatic/Playwright).
  5. 05Gates — axe in CI on stories; contrast checks; forbidden CSS patterns.
  6. 06Versioning — semver for the package; breaking changes rare and loud (RFC).
  7. 07References — Primer, Polaris, Spectrum, Radix, shadcn for open patterns.
css
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 ban

Working with design and PM

Translate Figma tokens into code tokens; push back on drag-only interactions (2.5.7); require focus states in designs; treat a11y bugs as P0/P1 with user impact, not “polish.” Interview story: time you blocked a ship for keyboard trap or contrast failure and proposed a fix path with budget and owner.

Testing strategy for a11y

  1. 01Automated: axe-core / eslint-plugin-jsx-a11y — catch ~30–40%, not enough alone.
  2. 02Component tests: Testing Library with role queries (getByRole) — forces semantics.
  3. 03E2E: keyboard path for critical journeys in Playwright.
  4. 04Manual: SR pass on new patterns; zoom 200%; Windows high contrast spot check.
  5. 05Storybook a11y addon on every primitive story before merge.

Failure modes (a11y production)

  1. 01Dialog without focus trap and without focus restoration.
  2. 02Icon-only button with missing or generic aria-label.
  3. 03Color contrast below 4.5:1 for body text.
  4. 04Form errors not linked via aria-describedby + aria-invalid.
  5. 05Live region spam on every stream token.
  6. 06Touch targets under 24×24 CSS px.
  7. 07Cognitive CAPTCHA as only auth path (3.3.8 risk).

Interview answer bank — a11y & design systems

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.”
Open design-system references worth citing: GitHub Primer, Shopify Polaris, Adobe Spectrum, Radix primitives, and shadcn/ui for composition patterns. In interviews, “I would wrap Radix/React Aria for behaviour and apply our tokens for visuals” is a senior answer that shows you will not reinvent focus management under a time box.
text
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 pattern
Stewardship story for behavioural rounds: “A team shipped a div modal that trapped keyboard users. I landed a DS Dialog with trap/restore, added an eslint ban on role=dialog outside the package, and put an axe story in CI. Incident class closed, not just the ticket.” That is ownership, not a component PR.

APG patterns you must name cold

Composite widgets that show up in machine coding and system design: combobox, listbox, dialog modal, tabs, menu, disclosure. For each, know: roles, essential keyboard map, focus movement model (roving tabindex vs activedescendant), and the one production bug that usually ships (missing Escape, missing restore focus, missing aria-expanded). Linking to w3.org/WAI/ARIA/apg/patterns/ in an interview is fine — memorising every attribute is less important than knowing which pattern you are implementing and not freestyling roles.
  1. 01Combobox — input + popup listbox; arrows; Enter select; Escape close; aria-expanded/controls/activedescendant.
  2. 02Dialog — trap focus; Escape; restore opener; aria-modal; labelled by title.
  3. 03Tabs — tablist/tab/tabpanel; arrows among tabs; Tab moves into panel.
  4. 04Menu — menuitem roles; arrows; Escape; careful not to use menu for site nav (use navigation landmarks).
  5. 05Disclosure — button aria-expanded controls the region; native details/summary often enough.
Colour is not a complete channel. Status, charts, and validation must not encode meaning by colour alone — pair with text, icons with accessible names, or patterns. Contrast: body text 4.5:1, large text 3:1 as a baseline AA rule of thumb. Design tokens should expose semantic danger/success colours that already pass contrast on their intended surfaces.
Motion: respect prefers-reduced-motion for decorative animation; keep essential state changes visible without relying on animation alone. Streaming and live dashboards: polite live regions for summary status, never per-tick assertive spam. Target size 24×24 CSS px minimum at AA (2.5.8) with documented exceptions — push back on 12px icon hit areas in Figma.
tsx
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])
Screen reader testing ritual: for every new composite widget, run one VoiceOver or NVDA pass against the APG expectations before merge. Check name, role, value, expanded state, and that focus is never lost to a black hole. Pair with Testing Library getByRole tests so regressions fail in CI. Automated axe is necessary and insufficient — say that sentence in senior interviews.
docsWAI-ARIA Authoring Practices Guide (APG)W3CdocsAPG — Combobox patternW3CdocsAPG — Dialog (Modal) patternW3CdocsWCAG 2.2W3CdocsThe A11Y ProjectA11Y ProjectdocsStorybook a11y addonStorybook

Checkpoint

A custom div with onClick and aria-label is used as a primary action. What is the architectural a11y failure?

AMissing CSS hover style only — cosmetic.BReimplementing a button without full keyboard, role, and form semantics — should be a native button (or complete APG widget).CIt fails only colour contrast.
Sign up free to answer and see why

Checkpoint

When is ARIA a smell?

AUsing aria-expanded on a disclosure that uses a real button.BUsing role=button on a div instead of , or contradictory ARIA that fights native semantics.CAny use of aria-live.
Sign up free to answer and see why

Checkpoint

WCAG 2.2 Target Size Minimum (AA) most directly requires…

AAll icons to be 48×48 SVG viewBoxes.BPointer targets at least 24×24 CSS pixels (with documented exceptions).CTouch screens only — desktop can use 1×1 targets.
Sign up free to answer and see why

Checkpoint

You own a design system. A team wants a one-off modal that traps focus incorrectly. Best stewardship move?

ALet them ship — DS purity is less important than velocity.BProvide / require the DS Dialog primitive with correct focus trap + restore; ban one-off modal divs via review/lint.CDelete all modals from the product.
Sign up free to answer and see why

Checkpoint

Best automated + human testing combo for a new combobox?

AOnly axe on the Storybook page — ship if green.Baxe + Testing Library role queries + keyboard E2E path + one VoiceOver/NVDA pass against APG expectations.CVisual regression screenshots alone.
Sign up free to answer and see why

Can you name WCAG 2.2 FE-owned criteria and defend semantic HTML vs ARIA in a design-system context?

New to itGetting thereConfident

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.

Accessibility and design-system stewardship · Frontend…