I spent three days last year auditing a checkout flow that had a 4.8/5 design rating in Figma. Visually, it was tight. But keyboard users couldn't tab into the promo code field - a <div> with a click handler, no tabindex, no role. Screen reader users heard 'unlabeled' on every input because the designer had used placeholder text as the label and the dev had copy-pasted the pattern. The error messages showed in red but had no programmatic association with the fields. The 'Place order' button was inside a custom component that intercepted Enter but not Space. Every single one of these bugs was invisible to anyone who used a mouse.
That's the consistent pattern with accessibility failures: they're invisible to the majority of the team and completely blocking for a specific group of users. In this post I walk through how screen readers actually parse the DOM, what WCAG 2.2 requires in concrete implementation terms, which keyboard navigation patterns break most often, and how to build a testing workflow that catches these issues before users do. I'm writing this from a frontend engineering perspective - I'll point to the spec where it matters, but the focus is on what breaks in real products and how to fix it.
Part I - Who Uses Assistive Technology and Why It Matters Beyond Compliance
The framing of accessibility as a 'disability' concern misses most of the population it affects. The WHO estimates that 1.3 billion people - 16% of the global population - live with some form of disability. But the spectrum is much wider than permanent conditions: someone with a broken arm uses keyboard navigation. Someone in bright sunlight needs high contrast. Someone with a concussion can't process dense, fast-moving content. Someone in a loud environment uses captions. Inclusive design that handles these cases well doesn't just serve people with disabilities - it produces better interfaces for everyone under suboptimal conditions.
- Screen reader users: Approximately 7.6 million people in the US have visual disabilities (US Census Bureau). Globally, the WHO reports 285 million people with visual impairments. Screen reader market share: JAWS (38%), NVDA (31%), VoiceOver (iOS/macOS, 18%), TalkBack (Android, 7%). These aren't edge cases - NVDA is free, VoiceOver ships with every Apple device, TalkBack is built into Android. Your users are already using these tools.
- Keyboard-only users: Motor impairments affect approximately 2 million Americans who use alternative input devices - switch controls, sip-and-puff devices, eye-tracking systems. All of these emulate keyboard input at the OS level. If keyboard navigation is broken, the entire product is inaccessible to this group.
- Cognitive and learning disabilities: Dyslexia affects 15–20% of the population. ADHD affects 8–10% of adults. Both are served by clear typographic hierarchy, predictable navigation patterns, visible focus states, and the ability to pause or control time-based content. These improvements also reduce cognitive load for neurotypical users in high-stress contexts.
- Legal exposure: The ADA (US), EN 301 549 (EU), and the Equality Act (UK) all establish legal requirements for digital accessibility. ADA lawsuits against websites exceeded 4,600 in 2023, up 42% year-over-year (UsableNet). The target WCAG conformance level for legal compliance is WCAG 2.1 AA in most jurisdictions; WCAG 2.2 AA is the current W3C recommendation.
- Business impact: The global disposable income of people with disabilities and their immediate social networks is estimated at $13 trillion (Accenture). Sites that fail basic accessibility checks lose this segment. More practically: every WCAG failure is a usability failure that affects a broader group than just people with disabilities.
Part II - How Screen Readers Actually Parse the DOM
The mental model that clears up most ARIA mistakes: screen readers don't read HTML - they read the accessibility tree. The accessibility tree is a parallel structure derived from the DOM that browsers build and expose through platform-specific APIs (MSAA/UIA on Windows, NSAccessibility on macOS, AT-SPI on Linux). Each node in this tree has four core properties: role, name, state, and value. When you interact with a screen reader, you're navigating this tree, not the DOM.
- Role: Derived from the HTML element tag (a
<button>has role 'button', a<nav>has role 'navigation') or overridden byroleattribute (<div role='button'>). The role tells the screen reader what kind of control this is and what interactions are expected. A<div>with no role is a 'generic' container - screen readers in browse mode will read its text content, but in interactive mode users won't find it via 'B' (next button) or 'F' (next form field) keyboard shortcuts. - Name: The accessible name is what the screen reader announces when focus reaches the element. Sources in priority order:
aria-labelledby(references another element's text),aria-label(explicit string), the element's content (for buttons and links), the associated<label>(for form inputs),titleattribute (fallback, not reliable). Placeholder text is NOT a name source - it disappears on input and is never reliably announced. This is the most common labeling bug. - State: Whether a control is expanded/collapsed (
aria-expanded), checked/unchecked (aria-checked), invalid (aria-invalid), disabled (disabledoraria-disabled), or selected (aria-selected). State must be programmatically updated when it changes - a custom accordion that visually opens must also togglearia-expandedon the trigger. Visual-only state changes are invisible to screen readers. - Browse mode vs. interactive mode: Screen readers have two primary modes. In browse mode (NVDA/JAWS default on page load), arrow keys move through the accessibility tree linearly, reading all content. In interactive mode (forms mode), keys are captured by the focused control instead of the screen reader. The transition is automatic: focusing a text input switches to interactive mode; pressing
Escapereturns to browse mode. This is why keyboard-operable controls must use native semantics - a<div>withonclickwon't trigger form mode and users can't interact with it. - Virtual cursor vs. focus: In browse mode, screen readers maintain a 'virtual cursor' position separate from the browser's keyboard focus. The virtual cursor can be on any element, even non-focusable ones. This is why you can't rely on CSS
:focusto indicate what a screen reader is currently reading - only what the keyboard focus is on. ARIA live regions (covered in Part VII) are how you announce dynamic content changes without requiring the user to navigate to a specific element.
Part III - Keyboard Navigation: The Patterns That Break Most Often
Keyboard navigation has a well-defined spec - the ARIA Authoring Practices Guide (APG) describes the expected keyboard behavior for every widget type. The failures I see in production almost always come from custom components that implement some keyboard behavior but not all of it, or from focus management that's never been thought through.
- Tab order and DOM order: The default tab order follows DOM source order, not visual layout. CSS grid and flexbox let you visually reorder elements without changing DOM order - this creates a disconnect where the visual reading order and keyboard tab order diverge. Rule: DOM source order should match the intended reading order. If visual reordering is unavoidable,
tabindexcan be used, but managingtabindexmanually across a complex layout is fragile. - Focus traps in modals: When a modal opens, keyboard focus must be trapped inside it. Users pressing
Tabshould cycle through focusable elements within the modal only - reaching the last one should wrap to the first, not escape to the page behind. When the modal closes, focus must return to the trigger element that opened it. Implementing this requires: capturingTabandShift+Tabkeydown events, querying all focusable descendants (button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])), and managing wrap-around. Libraries likefocus-traphandle this correctly. Rolling it manually is where bugs appear. - `tabindex` misuse:
tabindex='0'makes an element focusable in DOM order (use for custom interactive elements that aren't natively focusable).tabindex='-1'makes an element programmatically focusable (via.focus()) but removes it from the tab order (use for elements that receive focus programmatically, like modal containers).tabindexvalues greater than 0 are a legacy pattern that creates unpredictable tab order and should never be used. - Keyboard interactions for custom widgets: The APG defines expected keyboard patterns. Custom dropdown/select:
EnterorSpaceopens the menu; arrow keys navigate options;Enterselects,Escapecloses, focus returns to trigger. Custom tabs: arrow keys switch tabs within the tab list (roving tabindex pattern);Tabmoves focus to the tab panel. Accordion:EnterorSpacetoggles. Implementing half of this is worse than implementing none of it - a dropdown that opens withEnterbut requires mouse to select an option is a keyboard trap. - Skip navigation links: The first focusable element on every page should be a 'Skip to main content' link that jumps focus past the navigation. Without it, keyboard users on pages with complex navigation must tab through every nav item on every page load. The link is typically visually hidden and revealed on focus. Implementation:
<a href='#main-content' class='sr-only focus:not-sr-only'>Skip to main content</a>plus<main id='main-content' tabindex='-1'>. Thetabindex='-1'on<main>ensures the<main>element can receive programmatic focus (.focus()) without appearing in tab order. - Focus visibility: WCAG 2.2 Success Criterion 2.4.11 (Focus Not Obscured, Level AA) requires that when a component receives focus, it isn't entirely hidden by sticky headers, cookie banners, or fixed-position overlays. Use CSS
scroll-margin-topto account for sticky header height. CSS:focus-visibletargets keyboard focus specifically (not mouse click focus) - use it to provide clear focus indicators without affecting mouse users. The default browser focus ring has been the correct answer since Chrome 86 and Safari 15.4 - the oldoutline: nonepattern is a WCAG failure.
Part IV - WCAG 2.2: The Criteria That Actually Show Up in Audits
WCAG 2.2 has 78 success criteria across three conformance levels (A, AA, AAA). Level AA is the legal standard in most jurisdictions and the practical target for most products. I'm not going to walk through all 78 - instead, here are the ones that account for the majority of failures I see in production audits, with the implementation specifics that matter.
- SC 1.1.1 Non-text Content (Level A): Every image must have a text alternative. For informative images:
altdescribes the content and function. For decorative images:alt=''(empty string, not missing) so screen readers skip it. For functional images (linked icons, icon buttons):altoraria-labeldescribes the action, not the visual. Common failure: an icon button with no text and noaria-label. Screen readers announce 'button' with no name - unusable. For SVG icons used as button content:<svg aria-hidden='true'>on the icon plus visible or visually-hidden text, oraria-labelon the button. - SC 1.4.3 Contrast (Level AA): Normal text requires 4.5:1 contrast ratio against its background. Large text (18pt / 14pt bold and above) requires 3:1. UI components (input borders, button boundaries) require 3:1 against adjacent colors. Common failures: light gray placeholder text, white text on light brand colors, disabled elements with insufficient contrast (WCAG 2.2 exempts disabled components from contrast requirements - but only if they are genuinely disabled, not just visually styled that way).
- SC 1.4.4 Resize Text (Level AA): Text must be readable when zoomed to 200% without loss of content or functionality. This fails when font sizes are set in
pxinstead ofrem(browser zoom changes viewport, not pixel sizes), when containers have fixed heights that clip text on zoom, or when text is in SVGs that don't scale. - SC 2.1.1 Keyboard (Level A): All functionality must be operable via keyboard. The failure mode: custom interactive components implemented with mouse-only event handlers. Every
clickhandler on a non-native element needs a correspondingkeydownhandler forEnterand oftenSpace. Native<button>and<a>elements handle this automatically - which is the core argument for preferring native elements over ARIA-augmented divs. - SC 2.4.3 Focus Order (Level A): Focus must move in an order that preserves meaning and operability. Violated when modals append to
<body>without focus management, when dynamic content is inserted before the user's current focus position without announcement, or whentabindexvalues create illogical tab sequences. - SC 2.4.7 Focus Visible (Level AA) and SC 2.4.11 Focus Not Obscured (Level AA, new in 2.2): Focus must be visible (don't remove
outline), and the focused element must not be entirely hidden by overlapping content like sticky headers. SC 2.4.12 (Level AAA) goes further - the focused element must have no part obscured. - SC 3.3.1 Error Identification (Level A) and 3.3.2 Labels (Level A): When an input error is detected, the item in error must be identified and the error described in text - not just with color, not just with an icon. The error message must be programmatically associated with the input via
aria-describedby(oraria-errormessagewitharia-invalid='true'). The label must identify the purpose of the input. Placeholder text fails SC 3.3.2 because it disappears and doesn't meet the minimum color contrast requirement in most browsers. - SC 4.1.2 Name, Role, Value (Level A): For all UI components, the name, role, and current state must be determinable by assistive technologies. This is the catch-all for custom widgets: every interactive component must expose its role (via native semantics or
roleattribute), its accessible name, and its current state (aria-expanded,aria-selected,aria-checked, etc.).
Part V - Common Production Failures and the Fix Pattern
These are the failures that appear in nearly every accessibility audit I've done on production eCommerce and SaaS frontends. Each one has a consistent fix pattern.
- `<div>` and `<span>` buttons:
<div onClick={handler}>Click me</div>- no role, no keyboard access, no focus. Fix: use<button type='button'>for all interactive controls that aren't navigation links. If you can't change the element, addrole='button' tabindex='0' onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handler(e); }}. But native<button>is always better - it handles all of this automatically, works withformsubmit, and has native disabled state. - Icon-only buttons with no accessible name:
<button><Icon /></button>announces as 'button' with no label. Fix: addaria-label='Close dialog'on the button, or add visually-hidden text:<span class='sr-only'>Close dialog</span>. Setaria-hidden='true'on the icon SVG so it's skipped. Never usetitleattribute as the primary label - it's not consistently announced and doesn't appear on touch devices. - Form inputs without persistent labels: Using placeholder as the only label. Fix: always use
<label for='input-id'>oraria-label. Group related inputs with<fieldset>and<legend>(radio groups, checkbox groups). Associate error messages:<input aria-describedby='email-error' aria-invalid='true' /> <p id='email-error'>Enter a valid email address</p>. Thearia-invalidattribute triggers screen readers to announce 'invalid entry' when the input is focused. - Color-only information: Error states shown only in red, required fields marked only with a red asterisk, status badges distinguished only by color. Fix: supplement color with a text label, pattern, icon with alt text, or other non-color indicator. The red asterisk for required fields must have an explanation:
<span aria-hidden='true'>*</span><span class='sr-only'>(required)</span>or a visible legend before the form. - Dynamic content changes with no announcement: AJAX-loaded results, toast notifications, live search results updating - none of these trigger a screen reader announcement unless you use ARIA live regions. Fix: add
aria-live='polite'to the container that receives dynamic updates. For status messages (search results count, form submission success):<div role='status' aria-live='polite'>{resultCount} results found</div>. For urgent alerts:role='alert'(implicitaria-live='assertive'). The container must exist in the DOM before the update - inserting a live region and populating it in the same tick may not trigger announcement. - Infinite scroll with no keyboard escape: A common eCommerce pattern. Once keyboard users enter an infinite scroll list, they're stuck - the list keeps growing as they tab through it with no exit. Fix: either use pagination (genuinely accessible by default), or provide a 'Load more' button at the end of the current batch (keyboard users can reach it and choose to continue), or implement a 'Jump to end of results' skip link.
- Auto-playing video with no controls: Violates SC 1.4.2 (Audio Control) and SC 2.1.1. Background videos must not auto-play with sound. If they auto-play without sound, they must have a pause mechanism. Videos with narration must have captions (SC 1.2.2).
Part VI - Testing: The Workflow That Catches Real Issues
Automated tools catch somewhere between 30% and 40% of WCAG failures. That sounds bad, but it's still worth running them in CI - they catch the mechanical failures (missing alt text, contrast violations, missing labels) at zero marginal cost. The other 60% requires manual testing, and specifically: keyboard-only navigation and screen reader testing. Here's the workflow I use.
- Automated: axe-core in CI:
axe-coreis the engine behind most accessibility testing tools. Integrate it via@axe-core/playwrightor@axe-core/reactfor component-level testing. In CI: runaxeon every page in your routing structure and fail the build on anycriticalorseriousviolations. Sample Playwright setup:const results = await new AxeBuilder({ page }).analyze(); expect(results.violations.filter(v => ['critical','serious'].includes(v.impact))).toHaveLength(0);. This catches missing labels, contrast failures, ARIA misuse, and role violations automatically. - Browser tooling: Accessibility panel in DevTools: Chrome DevTools has an Accessibility panel (Elements > Accessibility) that shows the accessibility tree for any element and its computed name, role, and state. Use this to debug why a screen reader isn't announcing the name you expect. The Lighthouse accessibility panel gives a quick 0–100 score and a prioritized list of violations - but don't treat the score as a conformance level. A 97 score does not mean WCAG 2.1 AA compliance.
- Keyboard testing: mouse-free navigation: Tab through every interactive element on the page. Every element must be reachable, visible when focused, and operable with
Enter/Space. Test modals: does focus move into the dialog on open? DoesEscapeclose it? Does focus return to the trigger on close? Test forms: can you submit withEnter? Are validation errors reachable? Test custom widgets: do they follow the APG keyboard patterns? This takes 15–30 minutes per page and catches the majority of non-automated failures. - Screen reader testing: VoiceOver and NVDA: On macOS: VoiceOver (
Command+F5) with Safari. Turn it on, close your eyes (or look away), and navigate the page using only keyboard and listening to announcements. Key checks: is every interactive element announced with a meaningful name and role? Are state changes announced (menu open/closed, item selected)? Are error messages read when they appear? On Windows: NVDA (free) with Firefox or Chrome. Use browse mode (arrow keys to read) and interactive mode (Tab to navigate to inputs). JAWS is the most-used screen reader in enterprise contexts but requires a license for extended testing. - Real user testing: Automated and manual testing by sighted developers catches the mechanical failures. For cognitive and perceptual accessibility, testing with actual users who rely on assistive technology surfaces issues that are impossible to find otherwise. Even one session per quarter with a screen reader user catches systemic patterns that automated tools miss entirely.
Part VII - Accessible Component Patterns
These are the components that appear in almost every frontend product and have well-defined accessible patterns. Getting these right means most of your accessibility work is done at the component level - pages built from accessible components are accessible by composition.
- Modal dialogs: Required attributes:
role='dialog'(orrole='alertdialog'for confirmations),aria-modal='true'(tells screen readers to treat content outside as inert),aria-labelledbypointing to the dialog's heading. Required behavior: focus moves into dialog on open (to the first focusable element or the dialog container itself), focus is trapped inside,Escapecloses dialog, focus returns to trigger on close. The HTML<dialog>element handles most of this natively in modern browsers - use it where possible.<dialog open>provides focus management,Escapebehavior, and the::backdroppseudo-element. Current browser support: 96% globally (caniuse.com, 2026). - Form validation with live error messages: Pattern: validate on
blurfor individual fields, validate all on submit. On error: setaria-invalid='true'on the input, inject the error message into a<p id='field-error'>element, associate it witharia-describedby='field-error'on the input. For summary errors (multiple fields failed at once): inject anaria-live='assertive'summary at the top of the form, move focus to it, and list all errors as links that jump to the offending field. This ensures screen reader users hear the errors immediately and can navigate to each field. - ARIA live regions for toast notifications:
<div role='status' aria-live='polite' aria-atomic='true'>announces content changes without interrupting what the screen reader is currently reading.role='alert'(equivalent toaria-live='assertive') interrupts immediately - use only for critical errors, not routine notifications.aria-atomic='true'tells the screen reader to announce the entire region content on each update, not just the changed portion. Mount the live region in your root layout component so it's always present; update its content when a notification fires. - Custom dropdowns vs native `<select>`: Native
<select>is fully accessible everywhere, handles all keyboard navigation, works on touch and desktop, and requires zero ARIA. The only reason to build a custom dropdown is styling freedom - which is a valid reason, but understand the cost: you're reimplementing keyboard navigation (arrow keys, type-ahead,Escape,Enter), focus management, and all ARIA states. If you build custom: use the combobox pattern from the APG, test with VoiceOver and NVDA, and accept that it will require ongoing maintenance as browsers evolve. - Data tables: Use native
<table>,<th>,<td>,<caption>. Column headers needscope='col', row headers needscope='row'. For complex tables with multi-level headers, useid/headersassociation. The<caption>provides the accessible name for the table. Avoid CSS grids or divs for tabular data - screen readers won't interpret them as tables unless you add full ARIA table semantics, which is significantly more work than using native elements. - Carousels and sliders: The most-misused widget in eCommerce. Requirements: pause button for auto-rotating content (SC 2.2.2), previous/next controls with descriptive
aria-label('Next slide: Product 3 of 8'),aria-live='polite'on the slide container for non-auto-rotating carousels, keyboard arrow key navigation between slides. The honest answer: most product carousels fail accessibility so consistently that a static grid of products is the more accessible and often higher-converting alternative.
Part VIII - Accessibility in Next.js App Router
Next.js App Router introduces some accessibility considerations specific to its rendering model. The major one is route transitions: client-side navigation doesn't trigger the same page-load focus reset that browser navigation does. In traditional multi-page apps, navigating to a new page moves focus to the top of the page automatically. In a Next.js app, clicking a <Link> component updates the page content without a full reload - which means focus stays wherever it was, and screen reader users may not notice the page has changed.
- Route focus management in App Router: Next.js App Router manages focus on navigation automatically since Next.js 14 - it moves focus to the
<body>element on route change, which triggers screen readers to announce the new page title. This is better than previous behavior but less ideal than moving focus to the first heading on the new page. If your app has a persistent layout with a sticky header, test that the skip-to-main-content link works after client-side navigation - the<main>element withtabindex='-1'should receive focus when the skip link is activated. - Server Components and ARIA: ARIA attributes in Server Components are server-rendered HTML - screen readers parse them from the initial HTML response, same as any other attribute. No special consideration needed. The issue arises with dynamic state:
aria-expanded='false'can be server-rendered, but toggling it totrueon user interaction requires a Client Component. The clean pattern: split at the interactive boundary. A<DisclosureButton>Client Component managesaria-expandedstate; the disclosure content can be a Server Component passed as children. - **
@radix-ui/react-*for accessible primitives:** Radix UI provides accessible, unstyled primitives for the common widget types:Dialog,DropdownMenu,Select,Tabs,Accordion,AlertDialog,Tooltip. Each primitive implements the full ARIA APG pattern for its widget type, including keyboard navigation, focus management, and state management. Using Radix (or similar: Headless UI, Ark UI, React Aria from Adobe) as your component foundation means you get correct ARIA behavior from the start and can focus styling effort rather than accessibility implementation. This is the most cost-effective approach for teams building design systems. - Accessibility and Core Web Vitals: There's a direct relationship. Cumulative Layout Shift (CLS) - content moving unexpectedly - is disorienting for users with cognitive disabilities and catastrophic for screen reader users whose virtual cursor position shifts when the layout changes. Long Tasks (the root cause of INP failures) delay focus movement and ARIA live region announcements, creating a lag between user action and screen reader feedback. Optimizing CWV for performance also improves accessibility for the same underlying reasons - the user experience of a slow, shifting layout is disproportionately bad for people who depend on predictable interaction timing. For the performance side: the web performance guide.
Conclusion
The accessibility debt in most production web applications comes from the same source: interactive components built with generic elements, labels that exist only visually, state changes that are invisible to the accessibility tree, and focus management that was never considered. None of these are difficult to fix individually - they're difficult to fix at scale after the fact.
The practical path forward: add axe-core to your CI pipeline this week - it's an hour of setup and it starts catching failures immediately. Add keyboard testing to your definition of done for any new interactive component. Start using native HTML elements where you're currently using <div> - <button>, <a>, <input>, <select>, <details> - and you get half of WCAG 2.2 AA for free. Use Radix UI or React Aria for complex widget types and skip the ARIA implementation work entirely.
For accessibility audits or accessible frontend implementation - accessibility audit service | technical SEO and structured data | discuss your project.
FAQ
- What's the difference between WCAG 2.1 and WCAG 2.2? WCAG 2.2 (published October 2023) adds nine new success criteria to WCAG 2.1, primarily around focus visibility, cognitive accessibility, and authentication. Key additions at Level AA: SC 2.4.11 (Focus Not Obscured - focused element can't be entirely hidden by sticky headers), SC 2.5.7 (Dragging Movements - drag-and-drop must have a pointer-only alternative), SC 3.2.6 (Consistent Help - help mechanisms appear in the same place across pages), and SC 3.3.8 (Accessible Authentication - no cognitive function test like puzzles or memory required for login). WCAG 2.2 is backward-compatible - conforming to 2.2 AA means conforming to 2.1 AA.
- Does `aria-label` override visible text? Yes.
aria-labelreplaces the computed name from element content. If you have<button aria-label='Close'>X</button>, screen readers announce 'Close button' not 'X button'. This is correct for icon buttons where the visible content (icon) isn't descriptive. Butaria-labelon an element whose visible text says something different creates a discrepancy that confuses users who can see the text and hear the screen reader. Usearia-labelonly when there's no visible text to use as the accessible name. - Is it enough to make a site work with one screen reader? No. VoiceOver + Safari, NVDA + Firefox, JAWS + Chrome, and TalkBack + Chrome all interpret ARIA differently in edge cases. The WebAIM Screen Reader User Survey (2024) shows JAWS and NVDA together represent 70% of desktop screen reader usage. At minimum, test with NVDA + Firefox and VoiceOver + Safari. Radix UI and React Aria components are tested across all major screen reader combinations.
- How do I handle third-party components that fail accessibility checks? First: file an issue with the library and check if there's a version that fixes it. For critical violations, wrap the component and add ARIA attributes at the wrapper level where possible -
aria-labelon a wrapper<div>won't fix missing button labels inside, butaria-describedbyassociations can be added at the usage site. As a last resort, build the component yourself or find an alternative library. Never add/* eslint-disable jsx-a11y */as a permanent fix - it hides the problem without solving it. - What automated accessibility score should we target? Lighthouse accessibility score above 95 is a reasonable floor for CI - anything below that indicates failures that automated tooling can detect. But don't treat the score as conformance - a site can score 100 in Lighthouse and still fail multiple WCAG 2.2 AA criteria that require manual testing (focus management, keyboard interaction, live region behavior). The score is a leading indicator, not a certification.
References
- W3C. 'Web Content Accessibility Guidelines (WCAG) 2.2': https://www.w3.org/TR/WCAG22/
- W3C. 'ARIA Authoring Practices Guide (APG)': https://www.w3.org/WAI/ARIA/apg/
- W3C. 'Using ARIA': https://www.w3.org/TR/using-aria/
- WebAIM. 'Screen Reader User Survey #10 (2024)': https://webaim.org/projects/screenreadersurvey10/
- WHO. 'Disability and Health Fact Sheet': https://www.who.int/news-room/fact-sheets/detail/disability-and-health
- UsableNet. 'ADA Web Accessibility Lawsuit Report 2023': https://usablenet.com/resources/reports/2023-mid-year-digital-accessibility-lawsuit-report
- MDN Web Docs. 'Accessibility': https://developer.mozilla.org/en-US/docs/Web/Accessibility
- Deque. 'axe-core accessibility engine': https://github.com/dequelabs/axe-core
- Radix UI. 'Accessible component primitives': https://www.radix-ui.com/
- Adobe. 'React Aria': https://react-spectrum.adobe.com/react-aria/
- Scott O'Hara. 'Accessible Components': https://www.scottohara.me/
- Léonie Watson. 'Screen Reader Behavior Research': https://tink.uk/
