Accessibility is engineering discipline, not compliance box-checking. This guide is the full practice: what WCAG requires in daily work, the four testing layers and how they complement each other, why the component library is the highest-leverage fix, framework-specific patterns, and how to move from crisis-sprint model to sustainable-daily-practice model. Practical over theoretical.
What accessibility discipline actually is
Accessibility discipline is treating a11y the same way you treat testing, security, or performance: as continuous engineering practice, not project. Every PR includes an a11y consideration; every design review includes a11y check; every new component is built accessibly from the start; regressions are prevented, not remediated.
Framed differently: the goal isn't “pass the audit;” the goal is “users with disabilities can use our product as effectively as users without.” The audit measures whether we're succeeding; passing the audit isn't the objective.
This framing matters because it changes what gets built. Compliance framing produces: last-minute remediation, patched-on aria-labels, brittle fixes that regress. Discipline framing produces: components accessible by construction, regressions caught continuously, users' actual experience improving.
WCAG in practice (what to actually check)
WCAG 2.1 AA has 50 success criteria. Most are covered by roughly 15 things you should actually check daily:
Perceivable: Alt text on informative images (empty alt on decorative). Sufficient color contrast (4.5:1 for text under 18pt; 3:1 above). Text resizable to 200% without breaking layout. Captions on video content.
Operable: All interactive elements reachable via keyboard. Focus visible (not outline: none without replacement). No keyboard traps. Skip navigation links for keyboard users. Adequate touch target sizes (44x44px minimum).
Understandable: Form fields have labels (real labels, not just placeholders). Error messages associated with fields. Consistent navigation across pages. Language of page declared (<html lang="en">).
Robust: Valid semantic HTML (buttons as <button>, not <div onclick>). ARIA used only when semantic HTML insufficient (ARIA is a fallback, not a decoration). Custom widgets follow WAI-ARIA patterns.
Most audit failures are one of these 15. Automated tools catch about 40% of them; humans catch the rest.
The four layers: audit, review, test, users
No single method catches everything. Layered defense:
Layer 1: automated audits catch mechanical issues (missing alt, contrast, unlabeled forms). Cheap; continuous; catches ~40%.
Layer 2: code review catches structural issues (semantic HTML choices, ARIA usage, keyboard handling). Cheap; per-PR; catches ~30% (overlap with layer 1).
Layer 3: keyboard + screen reader testing catches interaction issues (focus order, announcements, custom widget behavior). Not cheap; periodic; catches ~20%.
Layer 4: users with assistive tech catches real usability issues (does this actually work for a screen reader user's mental model?). Expensive; quarterly at most; catches ~10% (unique) but essential.
Skipping any layer creates blindspots. Skipping layer 1: mechanical issues accumulate. Skipping layer 2: structural drift over time. Skipping layer 3: custom widgets are unusable. Skipping layer 4: false confidence.
Layer 1: automated audits (what they catch)
axe-core (also underpinning Deque tools, Storybook a11y addon, React DevTools a11y) is the most widely-used engine. pa11y is a lighter alternative, integrates well with CI. Lighthouse includes axe-core internally.
Setup:
CI integration: Run on every PR. Baseline current state; block new violations. See /a11y-audit command for shift-left version.
Dev-time integration: @axe-core/react as dev dependency; violations logged in browser console during development. Immediate feedback.
Test integration: jest-axe or vitest-axe for component tests; assertions like expect(container).toHaveNoViolations().
What they catch well: contrast ratios, missing labels, invalid ARIA, missing alt attributes, insufficient heading structure, keyboard-inaccessible interactive elements (missing tabindex/role).
What they miss: focus order that doesn't match reading order, screen reader announcements that don't match visual UI, custom widgets with syntactically-valid but semantically-wrong ARIA, keyboard traps in dynamically-added content, drag-and-drop and other complex interactions.
Layer 2: code review discipline
Add a11y to your PR review checklist. Explicit items:
Semantic HTML first. Is this <div onclick> when it should be <button>? Is this heading level meaningful? Is this list actually a list? ARIA should be a fallback; semantic HTML is the primary tool.
Keyboard access. Can you Tab to every interactive element? Does Enter/Space activate buttons? Escape close modals? Arrow keys navigate lists (where appropriate)?
Focus management. When something opens, does focus move to it? When something closes, does focus return? Is focus visible always (never outline: none without replacement)?
Form patterns. Are labels associated with inputs (htmlFor in React)? Are error messages associated (aria-describedby)? Are required fields marked in a way screen readers pick up?
Color and contrast. Is color the only way information is conveyed (avoid: “click the red button”)? Are focus/hover states visible in high contrast?
Review discipline is per-PR overhead of maybe 30 seconds; catches structural issues automated audits miss.
Layer 3: keyboard + screen reader testing
Periodic testing (monthly minimum) with keyboard only and with screen reader. Not for daily development; for verification.
Keyboard-only testing: Unplug mouse. Navigate the product entirely with keyboard. Note: elements you can't reach, focus order confusion, keyboard traps, missing skip links. An hour catches a lot.
Screen reader testing: NVDA (Windows, free), VoiceOver (macOS/iOS, built-in), TalkBack (Android, built-in), JAWS (Windows, commercial). Learn basic navigation (Tab, arrow keys, headings navigation). Test critical user flows. Note: incorrect announcements, missed context, over-verbose descriptions.
You don't need expertise for periodic testing. Basic use catches most issues. Expertise matters for building custom widgets; basic use suffices for verification.
Layer 4: users with assistive tech
Quarterly or per-major-feature: recruit users with assistive tech. Screen reader users, keyboard-only users, users with low vision, users with motor impairments. Real usage; watch for pain points.
This layer catches issues no automation and no team-internal testing surfaces: mental model mismatches, real-world use patterns, product-level design issues that pass technical audit but fail users.
Recruit through: accessibility user testing services (Fable, AccessWorks, RollTide), disability advocacy organizations, in-country reviewers if international. Budget for it; it's not free but the ROI on catching high-impact issues is significant.
Component library as accessibility contract
The single highest-leverage a11y decision: make your component library accessible. Consumers of the library (your product engineers) get accessibility for free; can't accidentally build broken components.
What to build accessibly:
Buttons and links. Semantic elements; proper focus states; visible focus indicators.
Form inputs. Labels required (or explicit opt-out); error patterns standardized; validation announcements consistent.
Modals and dialogs. Focus trap; escape closes; return focus on close; aria-modal; body scroll lock. Every rolled-your-own modal has these wrong; libraries (Radix UI, HeadlessUI, React Aria) get them right.
Dropdowns and menus. Arrow key navigation; type-ahead; correct roles (menu vs. listbox depending on semantic). Complex; use library.
Tables. Correct scope on headers; caption when appropriate; keyboard navigation for interactive tables.
Icons. Decorative icons: aria-hidden="true". Meaningful icons: accessible name via aria-label.
Building this once = permanent baseline. Rolling your own each time = permanent bug source. Component library is the strongest a11y investment.
Framework-specific patterns
Different frameworks have different a11y idioms.
React: useId hook for stable IDs across renders. htmlFor for label association. React Aria library for complex widgets. react-19-expert subagent for pattern-specific questions.
Vue: Refs for focus management. Composition API works well for a11y state. Vue I18n integration considerations. vue-3-expert subagent.
Angular: CDK a11y module (focus trap, focus monitor, live announcer). Angular Material components built accessibly.
Svelte: Bind directives for focus. Store-based focus tracking. Growing a11y libraries.
Next.js: next-intl for locale-aware content; next/link handling of focus on route transitions.
Framework-specific patterns matter; a11y in one framework isn't a direct translation to another.
Custom widget discipline (WAI-ARIA patterns)
Building custom widgets (combobox, listbox, tree, tab, disclosure) is where most a11y bugs live. The WAI-ARIA Authoring Practices Guide defines the correct patterns for each widget type.
Discipline:
Look up the pattern first. Building a combobox? Read the combobox pattern. Building a menu? Read the menu pattern. Don't guess.
Match keyboard interactions to the pattern. Combobox: Down arrow opens listbox, up/down navigates options, Enter selects, Escape closes. Every combobox users have used works this way; yours must too.
Match ARIA roles and attributes. Combobox: role="combobox", aria-expanded, aria-controls, aria-activedescendant. Missing any means screen readers don't announce correctly.
Test with screen reader. Custom widgets' correctness must be verified with actual screen reader; automated tests catch syntax not semantics.
Rule of thumb: if a widget exists in a mature library (React Aria, HeadlessUI, Radix UI), use it. Rolling your own means implementing a WAI-ARIA pattern; expensive; usually not necessary.
Baseline and regression discipline
Existing codebases have pre-existing issues. Two extreme responses fail:
Fail extreme #1: ignore all pre-existing issues; only focus on new code. Existing issues never get fixed.
Fail extreme #2: block all PRs on all a11y issues; team can't ship anything.
Middle path:
Baseline current state. Run full audit; commit the results as a baseline file. Documentation of “this is where we are.”
Block regressions. New PRs cannot introduce new violations (violations not in baseline).
Reduce baseline incrementally. Every touch to code triggers baseline update; over time, baseline shrinks. Track baseline count as metric.
This model let us go from 400 issues to 40 over 20 months without a crisis sprint. Sustainable pace.
Metrics that matter
Track these; review monthly:
Baseline violation count: trending down over time.
Regression rate: PRs that introduced (and were caught by) new violations. Zero is achievable; each caught regression prevents production bug.
Screen reader tests per quarter: at least one; more for critical features.
User testing sessions per quarter: at least one with real assistive-tech users.
A11y-related support tickets: trending down as maturity increases.
Time from a11y bug report to fix: not different from other bug SLAs.
Not tracking: number of automated audit issues (goes down as CI matures; not a signal), number of aria-* attributes (over-use is worse than under-use).
What to do next
If a11y is currently ignored: set up axe-core in CI. Baseline current state; block regressions. That's the foundation.
If a11y is compliance-sprint model: shift toward continuous. Same tool (axe-core in CI); different mental model. Reduce baseline incrementally.
If a11y is continuous but weak: audit your component library. Make sure primitives are accessible. Then add code review discipline.
If component library and code review are solid: add keyboard-only + screen reader testing to your routine. Monthly. Catches what automation misses.
If all layers are in place: recruit assistive-tech user testing. Quarterly. Catches what internal testing doesn't.
Combined with /a11y-audit command, react-19-expert subagent (or your framework equivalent), and this discipline, a11y becomes background practice instead of periodic crisis.