WCAG 2.2 AA for React: The Component-Level Audit Checklist Reviewers Use
WCAG 2.2 AA is easy to claim and hard to prove at the component level. Automated scanning tools - axe-core, Lighthouse, Deque's browser extension - catch roughly 30 to 40 percent of WCAG failures. The rest require manual testing, keyboard walkthroughs, and knowing where React component libraries tend to break. This checklist covers what reviewers actually look for when auditing a React codebase for WCAG 2.2 level AA conformance, with the specific patterns that pass automated scans but still fail a real accessibility review.
What WCAG 2.2 Added That React Teams Frequently Miss
WCAG 2.2 (published October 2023 and now the reference standard for the European Accessibility Act) introduced several new success criteria at the AA level. Three of them cause consistent problems in React component libraries because they address behaviors automated tools cannot reliably detect.
2.4.11 Focus Not Obscured (Minimum) requires that focused components are not entirely hidden by sticky headers, cookie banners, or floating UI elements. A component can pass automated focus testing - it receives focus, the focus ring is visible - and still fail this criterion because the focused element is scrolled under a fixed nav bar the moment focus moves to it. In React applications with sticky layouts, this is common enough that it should be the first thing checked after automated scanning.
2.5.3 Target Size (Minimum) requires pointer targets to be at least 24x24 CSS pixels, unless the target is inline within text, or the spacing between targets provides the equivalent area. Icon-only buttons (close, expand, sort column headers), tags, and chip components in React UI libraries routinely fall below this threshold. The Tailwind utility p-1 on a 16px icon produces a 24px target - right at the limit. Anything smaller fails.
3.2.6 Consistent Help is less commonly triggered by component code and more by page-level navigation patterns, so it is lower priority in a component audit.
Focus Management: The Category Automated Tools Mostly Ignore
Focus management failures are the most consequential in React applications and the ones least likely to surface in automated scans. React's client-side routing, modal patterns, and conditional rendering all create situations where focus silently moves to the document body or disappears entirely.
Route changes. When a user navigates between pages using a Next.js Link or React Router, the new page content renders but focus stays wherever it was - often on a nav link that no longer exists in a meaningful way relative to the new content. The browser's native page navigation resets focus to the top; React's virtual navigation does not.
The standard fix is a focus management hook that moves focus to the page <h1> or a skip-link target after navigation:
// hooks/useFocusOnRouteChange.ts
import { useEffect, useRef } from 'react'
import { usePathname } from 'next/navigation'
export function useFocusOnRouteChange() {
const pathname = usePathname()
const mainHeadingRef = useRef<HTMLHeadingElement>(null)
useEffect(() => {
mainHeadingRef.current?.focus()
}, [pathname])
return mainHeadingRef
}
// In your page component:
// const headingRef = useFocusOnRouteChange()
// <h1 ref={headingRef} tabIndex={-1}>Page Title</h1>
Note tabIndex={-1} on the heading. Without it, focus() does nothing on non-interactive elements in most browsers. The element is programmatically focusable but not in the tab order - which is exactly what you want.
Modal dialogs. A dialog that opens must trap focus within itself (so Tab and Shift+Tab cycle only through dialog controls) and return focus to the trigger element when it closes. Both behaviors must be tested manually. The failure pattern is common: the dialog renders, focus stays on the button that opened it (or goes to the body), and Tab moves focus into content behind the overlay.
For custom dialogs not using the native <dialog> element or a library like Radix UI, the focus trap implementation needs to enumerate focusable descendants and intercept keydown events:
function useFocusTrap(ref: React.RefObject<HTMLElement>, active: boolean) {
useEffect(() => {
if (!active || !ref.current) return
const focusable = ref.current.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusable[0]
const last = focusable[focusable.length - 1]
first?.focus()
function handleKeyDown(e: KeyboardEvent) {
if (e.key !== 'Tab') return
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault()
last?.focus()
}
} else {
if (document.activeElement === last) {
e.preventDefault()
first?.focus()
}
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [active, ref])
}
If your component library uses Radix UI primitives, Dialog.Root and Dialog.Content handle focus trapping correctly. What still needs manual verification is focus return on close - Radix does return focus to the trigger when the dialog closes via the Escape key, but custom close handlers that bypass Radix's onOpenChange may not.
Expanding and collapsing content. Accordions, disclosure widgets, and expandable table rows need to keep focus on the control that triggered the state change. Focus should not jump to the newly revealed content (that disrupts reading flow) and must not be lost entirely.
Visible Focus Indicators: Beyond the Default Browser Ring
WCAG 2.2 AA requires focused components to have focus indicators that meet a minimum contrast ratio and visible area threshold (2.4.11 and 2.4.13). The common pattern of removing focus outlines globally (*:focus { outline: none }) and relying on mouse-focused designs fails outright.
:focus-visible is the right CSS pseudo-class to use in 2026 - it shows focus rings only during keyboard navigation, not on mouse click. But :focus-visible alone is not enough if the focus indicator has poor contrast or is too thin.
/* Minimum compliant visible focus - adjust colors to your brand */
:focus-visible {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
/* For dark backgrounds */
.dark-surface :focus-visible {
outline-color: #66b3ff;
}
The focus indicator must have at least 3:1 contrast against adjacent colors - both the component background and the surrounding page background. A white focus ring on a light gray button surface fails. Review focus indicators across your component variants, not just the default state.
Accessible Name Computation in Dynamic Components
Automated tools check whether interactive elements have accessible names. What they often miss is whether the accessible name changes correctly when component state changes, and whether computed names are meaningful in context.
Icon-only buttons. A button containing only an SVG icon must have an accessible name via aria-label. This is straightforward. What gets missed is that the label must describe the action, not the icon. aria-label="X" is not equivalent to aria-label="Close dialog".
Toggle buttons. A button that switches between two states - mute/unmute, show/hide, expand/collapse - needs to communicate the current state. There are two correct patterns:
// Pattern 1: aria-pressed (the button is the control)
<button aria-pressed={isMuted} onClick={toggleMute}>
{isMuted ? <MuteIcon /> : <SpeakerIcon />}
<span className="sr-only">{isMuted ? 'Unmute' : 'Mute'}</span>
</button>
// Pattern 2: aria-expanded (the button controls revealed content)
<button
aria-expanded={isOpen}
aria-controls="panel-id"
onClick={togglePanel}
>
Details
</button>
<div id="panel-id" hidden={!isOpen}>...</div>
The failure pattern is a toggle button whose visual label changes (the icon swaps) but whose aria-label is hardcoded to one state. Screen reader users hear "Mute" regardless of whether the audio is already muted.
Select components and comboboxes. Custom select implementations built from <div> and <ul> elements need a full ARIA widget pattern to be usable. The ARIA combobox pattern requires specific role assignments, keyboard event handling (arrow keys, Home/End, Escape), and live region announcements when the selected value changes. This is complex enough that using a tested library - Downshift, Radix Select, or React Aria's useSelect - is nearly always the right choice over building from scratch. If you have a custom implementation, auditing it against the ARIA APG combobox pattern specification is the fastest way to find what is missing.
The Failures That Only Surface in Manual Testing
Several categories of WCAG 2.2 AA failures are structurally undetectable by automated tools and require hands-on testing.
Keyboard trap detection. Tab through every interactive component with a keyboard only. Any point where focus is captured and cannot be moved forward with Tab or escaped with Escape is a Level A failure - but automated tools cannot detect dynamic traps that only occur in specific UI states.
Reading order vs. visual order. Screen readers follow DOM order, not visual layout. CSS Grid and Flexbox make it easy to visually reorder elements in ways that diverge from their DOM position. A grid-template-areas layout that places the sidebar content first in the DOM but last visually means keyboard and screen reader users encounter content in a different sequence than sighted users.
Timeout handling. WCAG 2.2 AA (2.2.1) requires that users can turn off, adjust, or extend any time limit longer than 20 hours - unless it is a real-time exception. Session timeouts, autosave countdown timers, and form expiry warnings all need to surface the time limit to users and give them a way to extend it. Automated tools cannot test whether your session timeout warning is announced to screen readers or whether the extension mechanism is keyboard accessible.
Focus obscured by sticky elements. As noted above, manually tab through pages with sticky headers and floating action buttons. Confirm that no focused element is entirely hidden behind a fixed-position element at any viewport width.
Running a Component Audit Before the European Accessibility Act Deadline
The European Accessibility Act applies to products and services offered in the EU, with enforcement beginning June 2025. For SaaS products with European customers, WCAG 2.2 AA conformance is no longer a best-practice recommendation - it is a legal baseline.
A component-level audit structured around this checklist typically takes one to three days depending on library size, and produces a prioritized list of issues by severity. High-severity issues (inaccessible interactive controls, missing accessible names, focus traps) directly block users with disabilities. Medium-severity issues (insufficient focus indicator contrast, minor focus management gaps) degrade usability without completely blocking access. The audit output gives development teams a clear remediation backlog rather than a pass/fail compliance stamp that is impossible to act on.
If your team is preparing for an accessibility review or needs an independent audit of your React component library, Wolf-Tech offers focused code quality consulting that covers accessibility alongside security, performance, and architectural patterns. Reach us at hello@wolf-tech.io or visit wolf-tech.io to discuss what a review of your specific codebase would cover.

