Skip to content

Web Accessibility in 2026: Screen Readers, WCAG 2.2, and What Actually Works in Production

Accessibility isn't a checklist you run at the end of a sprint. It's a set of structural decisions - semantic HTML, focus management, ARIA contracts - that either get baked in during development or cost three times as much to retrofit later. This post covers how it actually works in production.

Web accessibility illustration: screen reader, keyboard navigation, WCAG 2.2 and ARIA roles diagram
Published:Updated:Reading time:22 min read

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 by role attribute (<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), title attribute (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 (disabled or aria-disabled), or selected (aria-selected). State must be programmatically updated when it changes - a custom accordion that visually opens must also toggle aria-expanded on 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 Escape returns to browse mode. This is why keyboard-operable controls must use native semantics - a <div> with onclick won'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 :focus to 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, tabindex can be used, but managing tabindex manually across a complex layout is fragile.
  • Focus traps in modals: When a modal opens, keyboard focus must be trapped inside it. Users pressing Tab should 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: capturing Tab and Shift+Tab keydown events, querying all focusable descendants (button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])), and managing wrap-around. Libraries like focus-trap handle 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). tabindex values 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: Enter or Space opens the menu; arrow keys navigate options; Enter selects, Escape closes, focus returns to trigger. Custom tabs: arrow keys switch tabs within the tab list (roving tabindex pattern); Tab moves focus to the tab panel. Accordion: Enter or Space toggles. Implementing half of this is worse than implementing none of it - a dropdown that opens with Enter but 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'>. The tabindex='-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-top to account for sticky header height. CSS :focus-visible targets 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 old outline: none pattern 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: alt describes the content and function. For decorative images: alt='' (empty string, not missing) so screen readers skip it. For functional images (linked icons, icon buttons): alt or aria-label describes the action, not the visual. Common failure: an icon button with no text and no aria-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, or aria-label on 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 px instead of rem (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 click handler on a non-native element needs a corresponding keydown handler for Enter and often Space. 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 when tabindex values 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 (or aria-errormessage with aria-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 role attribute), 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, add role='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 with form submit, and has native disabled state.
  • Icon-only buttons with no accessible name: <button><Icon /></button> announces as 'button' with no label. Fix: add aria-label='Close dialog' on the button, or add visually-hidden text: <span class='sr-only'>Close dialog</span>. Set aria-hidden='true' on the icon SVG so it's skipped. Never use title attribute 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'> or aria-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>. The aria-invalid attribute 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' (implicit aria-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-core is the engine behind most accessibility testing tools. Integrate it via @axe-core/playwright or @axe-core/react for component-level testing. In CI: run axe on every page in your routing structure and fail the build on any critical or serious violations. 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? Does Escape close it? Does focus return to the trigger on close? Test forms: can you submit with Enter? 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' (or role='alertdialog' for confirmations), aria-modal='true' (tells screen readers to treat content outside as inert), aria-labelledby pointing 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, Escape closes 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, Escape behavior, and the ::backdrop pseudo-element. Current browser support: 96% globally (caniuse.com, 2026).
  • Form validation with live error messages: Pattern: validate on blur for individual fields, validate all on submit. On error: set aria-invalid='true' on the input, inject the error message into a <p id='field-error'> element, associate it with aria-describedby='field-error' on the input. For summary errors (multiple fields failed at once): inject an aria-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 to aria-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 need scope='col', row headers need scope='row'. For complex tables with multi-level headers, use id/headers association. 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 with tabindex='-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 to true on user interaction requires a Client Component. The clean pattern: split at the interactive boundary. A <DisclosureButton> Client Component manages aria-expanded state; 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-label replaces 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. But aria-label on an element whose visible text says something different creates a discrepancy that confuses users who can see the text and hear the screen reader. Use aria-label only 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-label on a wrapper <div> won't fix missing button labels inside, but aria-describedby associations 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

Related articles

The Universal Web Performance Architecture: A Systems-Level Analysis of 12 Engineering Pillars (2026 Edition)

A deep technical breakdown of web performance in 2026. Covers network physics, image pipelines, JavaScript execution models, the Critical Rendering Path, edge computing, Core Web Vitals, and production RUM - with real benchmark data and concrete implementation details.

EngineeringArchitectureCore Web Vitals
Read article

Partial Prerendering (PPR) in Production: Architecture Patterns (2026 Edition)

A deep dive into Next.js Partial Prerendering in production. Covers the two-phase response mechanism (static shell from CDN + streaming dynamic holes), Suspense boundary placement rules, PPR's interaction with the Full Route Cache, Suspense fallback design for zero CLS, measured TTFB/LCP outcomes, comparison against ISR+CSR and full SSR, known limitations, and the decision framework for when PPR is the right architecture choice.

Next.jsPerformancePPR
Read article

React 19 New Features: useOptimistic, use(), Server Actions Explained (2026 Edition)

A deep dive into React 19's five new primitives: useOptimistic (concurrent optimistic state with automatic rollback), use() (conditional Promise and Context reading), Server Actions ('use server' RPC mechanism and Progressive Enhancement), useActionState (state threading pattern replacing useFormState), and useFormStatus. Covers the action-based mutation architecture that replaces Redux/Zustand for server mutations, plus the ref-as-prop and Context-as-provider syntax changes.

React 19ArchitectureNext.js
Read article