A visitor using NVDA or VoiceOver hits your sign-up form, and the error message never gets announced. A keyboard-only user tabs into your modal and can’t tab back out. These are not edge cases anymore. WebAIM’s 2026 Million report found detectable WCAG failures on 95.9% of the top one million home pages, averaging 56.1 errors per page, up from 94.8% and 51 errors the year before. Meanwhile the European Accessibility Act has been enforceable since June 28, 2025, and US federal website lawsuits hit 3,117 in 2025 according to Seyfarth Shaw’s tracker, a 27% jump over 2024. This tutorial builds a working accessibility pipeline for both a React 19.2.8 project and a Vue 3.5.42 project, side by side, so you can see exactly where the two stacks diverge and where they don’t.
By the end you’ll have linting, runtime checks, automated Playwright scans, Storybook accessibility panels, focus management fixes, live-region announcements, contrast audits, and a CI gate that blocks merges on new violations, built out in both frameworks. Total time: about 90 minutes if you follow the steps in order. No prior accessibility experience is assumed, though basic familiarity with React or Vue component syntax helps.
Why Accessibility Compliance Can’t Wait Until 2026
Three things changed the calculus for teams that used to treat accessibility as a nice-to-have. First, the legal floor moved. WCAG 2.2 became a W3C Recommendation on December 12, 2024, and while WCAG 2.1 Level AA remains the version cited in most binding regulations (the EU’s EN 301 549 v3.2.1 and the US DOJ’s ADA Title II rule both still reference 2.1), regulators and courts increasingly treat 2.2 AA as the practical target teams should be building toward. WCAG 3.0 is still a Working Draft, last updated March 3, 2026, with a Candidate Recommendation not expected before late 2027, so there’s no near-term need to plan around it.
Second, enforcement got real outside the US. The European Accessibility Act applies to any company selling covered digital products or services to EU residents, regardless of where the company is based. The first EAA lawsuits landed in France in November 2025 against Auchan. The case was dismissed on procedural grounds in May 2026, but the court explicitly acknowledged the site’s inaccessibility. A more consequential ruling followed on June 4, 2026, when the Tribunal judiciaire de Caen ordered Carrefour to make its website and mobile app fully accessible within six months, backed by a daily penalty for delay. The Dutch Authority for Consumers and Markets has also been actively enforcing against e-commerce and telecom sites.
Third, the framework you pick appears to matter more than most teams assume. WebAIM’s 2026 Million breakdown by JavaScript framework found React-built home pages averaging 43.5 detectable errors, 22.5% below the all-site average of 56.1, while Vue.js pages averaged 64.6 errors and AngularJS pages averaged 76.6. That gap isn’t really about React being inherently more accessible than Vue. It reflects which teams adopt automated a11y tooling early and which don’t. This tutorial closes that gap for Vue teams by wiring up the same rigor React teams have had easier access to through tools like React Aria and the jsx-a11y ESLint plugin.
The US legal picture adds its own pressure, separate from the EU’s regulatory approach. Seyfarth Shaw’s ADA Title III tracker counted 3,117 federal website accessibility lawsuits filed in 2025, up 27% from 2,452 in 2024. UsableNet’s broader year-end report, which also counts state courts and mobile apps rather than just federal web cases, put the 2025 total closer to 4,928 to 5,114 filings depending on methodology, a jump of roughly 37% over the previous year. Mid-2026 filing pace suggests the industry is on track for more than 5,500 cases by year-end, with some projections running as high as 6,176 once every court and filing category is counted. None of these lawsuits ask which frontend framework a defendant used. They target the rendered result: missing labels, unreachable buttons, and contrast failures a visitor actually ran into.
Prerequisites: Versions and Tools You’ll Need
Lock in these versions before starting. Accessibility tooling moves fast enough that a plugin built against last year’s ESLint config can silently stop linting anything.
| Tool | Minimum Version | Version Used Here |
|---|---|---|
| Node.js | 18.x | 20.x LTS |
| React | 18.x | 19.2.8 (released July 21, 2026) |
| Vue | 3.x | 3.5.42 (npm latest as of September 2026) |
| react-aria-components | 1.x | 1.21.0 |
| axe-core | 4.10+ | 4.13.0 (released August 5, 2026) |
| @axe-core/playwright | 4.10+ | 4.13.0 |
| eslint-plugin-jsx-a11y | 6.x | 6.10.2 |
| eslint-plugin-vuejs-accessibility | 2.x | 2.6.0 |
| cypress-axe (optional) | 1.x | 1.7.0 |
One compatibility note before you touch a terminal: as of August 2026 there is no ESLint 10-compatible release of eslint-plugin-jsx-a11y yet. If your React project has already moved to ESLint 10, stay on ESLint 9 for the linting step below, or track the plugin’s GitHub issues for the compatibility release. You’ll also want a code editor with TypeScript support, a free GitHub account for the CI section, and at least one screen reader installed locally (NVDA on Windows is free, and VoiceOver ships built into macOS).
Two optional but genuinely useful additions: the axe DevTools browser extension, which lets you run a one-off scan on any live page without writing a test, and an editor extension that surfaces jsx-a11y or vuejs-accessibility warnings inline as you type rather than waiting for a full lint pass. Neither is required to follow this tutorial, but both shorten the feedback loop enough that most teams adopt them within the first week of setting up the rest of this pipeline. If you’re working inside a monorepo that houses both a React and a Vue app, install both ESLint plugins at the root and scope each one to its respective package via ESLint’s flat config files pattern, rather than maintaining two entirely separate lint configs.
Step 1 and 2: Scaffold Both Projects and Install Framework-Native Accessibility Libraries
Step 1: Scaffold the React and Vue Projects
Start both projects on Vite so the tooling in later steps (Storybook, Playwright, the dev server) behaves identically across frameworks.
npm create vite@latest a11y-react-demo -- --template react-ts
cd a11y-react-demo && npm install && cd ..
npm create vite@latest a11y-vue-demo -- --template vue-ts
cd a11y-vue-demo && npm install && cd ..
Step 2: Install React Aria for React, and the Vue Accessibility Stack for Vue
React doesn’t ship accessibility primitives out of the box, and neither does Vue. React Aria, maintained by Adobe, provides low-level hooks that implement WAI-ARIA design patterns (focus management, keyboard navigation, correct ARIA attributes) which you compose into your own components. Its higher-level sibling, react-aria-components, ships pre-built accessible components on top of those hooks. Version 1.21.0, released September 1, 2026, added a new NavigationTree component for hierarchical navigation menus with full keyboard support.
# React side
npm install react-aria-components@1.21.0
# Vue side — vue-axe wraps axe-core for runtime checks,
# and Headless UI Vue provides accessible unstyled components
npm install -D vue-axe
npm install @headlessui/vue
Vue’s ecosystem doesn’t have a direct equivalent to React Aria’s hook-based approach. Headless UI Vue is the closest match: it ships unstyled, accessible components (dialogs, listboxes, menus) with ARIA roles and keyboard handling already wired up, similar in spirit to what react-aria-components gives React. If you need lower-level primitives instead of full components, Radix Primitives and Ark UI are worth evaluating on the React side, though both are React-only, unlike Headless UI which ships both React and Vue builds.
It’s worth resisting the urge to build these primitives yourself. A dialog that correctly traps focus, restores it on close, closes on Escape, labels itself for screen readers, and blocks background scroll sounds simple until you’ve tried to write it from scratch and discovered how many of those five behaviors interact with each other in ways that break silently. React Aria and Headless UI have both been battle-tested against real assistive technology for years. A hand-rolled version usually gets 80% of the way there and then quietly fails the last, hardest 20%, which is exactly the part a screen reader user actually notices.
Step 3 and 4: Configure ESLint Accessibility Rules, Then Add Runtime axe-core Checks
Step 3: Configure ESLint Accessibility Rules for Both Frameworks
Catching accessibility mistakes at lint time, before a component ever renders, is the cheapest place to fix them. Install the framework-specific plugin in each project.
// React: install and wire up eslint-plugin-jsx-a11y (v6.10.2)
npm install -D eslint-plugin-jsx-a11y
// eslint.config.js (React)
import jsxA11y from 'eslint-plugin-jsx-a11y'
export default [
jsxA11y.flatConfigs.recommended,
{
rules: {
'jsx-a11y/no-autofocus': 'warn',
'jsx-a11y/label-has-associated-control': 'error'
}
}
]
// Vue: install and wire up eslint-plugin-vuejs-accessibility (v2.6.0)
npm install -D eslint-plugin-vuejs-accessibility
// eslint.config.js (Vue)
import vueA11y from 'eslint-plugin-vuejs-accessibility'
export default [
...vueA11y.configs['flat/recommended'],
{
rules: {
'vuejs-accessibility/label-has-for': 'error',
'vuejs-accessibility/no-autofocus': 'warn'
}
}
]
Both plugins cover roughly the same ground: missing alt text, unlabeled form controls, invalid ARIA attribute values, and interactive elements that aren’t keyboard-reachable. The rule names differ (jsx-a11y/alt-text versus vuejs-accessibility/alt-text) but the underlying WCAG success criteria they enforce are identical, which makes it straightforward to keep both configs in sync across a monorepo.
Start with each plugin’s recommended preset rather than hand-picking rules, then tighten specific rules to error once the team has cleared the initial backlog of warnings a first-time install typically surfaces. Turning every rule to error immediately on a large existing codebase tends to produce hundreds of failures overnight, which either blocks the whole team or gets the check disabled entirely within a week. A staged rollout, warnings first, then errors on new code, then errors everywhere, gets far more buy-in.
Step 4: Add Runtime axe-core Checks During Development
Linting catches static mistakes, but a lot of accessibility bugs only show up once a component actually renders (dynamic ARIA states, focus order, contrast against a computed background). Runtime axe-core checks catch those, logged straight to the browser console, and they run only in development, never shipping to production.
// React: main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
if (import.meta.env.DEV) {
import('@axe-core/react').then(axe => {
axe.default(React, ReactDOM, 1000)
})
}
ReactDOM.createRoot(document.getElementById('root')!).render()
// Vue: main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
if (import.meta.env.DEV) {
import('vue-axe').then(({ default: VueAxe }) => {
app.use(VueAxe)
})
}
app.mount('#app')
Watch out for what teams sometimes call the “@axe-core/react trap”: because it only scans the DOM after a 1000ms debounce and only in development, it will not catch violations introduced by fast, interactive state changes like a toast that appears and disappears in under a second. Treat runtime axe-core as a first pass, not full coverage. The automated tests in the next step close that gap.
The console-log format both tools use is identical to what you’ll see later from Playwright: a rule ID, an impact rating (minor, moderate, serious, or critical), and a CSS selector pointing at the offending element. Get comfortable reading that format now, since it’s the same shape you’ll be debugging in CI failures later in this tutorial.
Step 5 and 6: Automate Testing with Playwright, Then Add Storybook’s Accessibility Panel
Step 5: Automate Accessibility Testing with Playwright
@axe-core/playwright bundles axe-core internally and only needs Playwright as a peer dependency, so you don’t manage two separate version numbers. It works identically whether the page under test is React or Vue, because it scans the rendered DOM, not the framework’s virtual representation of it.
npm install -D @playwright/test @axe-core/playwright
npx playwright install
// a11y.spec.ts — works against either the React or Vue dev server
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test('signup page has no WCAG 2.1 AA violations', async ({ page }) => {
await page.goto('http://localhost:5173/signup')
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze()
expect(results.violations).toEqual([])
})
Point the same spec file at either dev server URL and run it twice, once per project, in CI. Running it against authenticated screens (not just the login page) matters, since UsableNet’s own analysis of lawsuit patterns shows most real complaints target logged-in flows like checkout and account settings, not marketing pages.
If your app requires a login before reaching the pages you care about most, save an authenticated storage state once with Playwright’s storageState feature and reuse it across every accessibility spec, rather than logging in fresh inside each test. That keeps the accessibility suite fast enough that nobody’s tempted to skip it locally before pushing a branch.
Step 6: Add Accessibility Checks to Storybook
The Storybook accessibility addon, @storybook/addon-a11y, runs axe-core against every story and shows violations mapped to WCAG rule IDs directly in an “Accessibility” panel. It works the same way in React and Vue Storybook instances, which makes it a good shared checkpoint for teams running both stacks.
npx storybook@latest add @storybook/addon-a11y
// .storybook/main.ts
export default {
addons: ['@storybook/addon-a11y']
}
Open any story and the panel flags issues at the component level, before that component ever ships inside a real page. Component-level catches are cheaper to fix than page-level ones because you’re looking at one isolated piece of UI instead of debugging which of a dozen nested components introduced the violation. Because both React and Vue Storybook instances use the exact same addon and the exact same underlying axe-core engine, a design system team maintaining components in both frameworks gets identical violation reports for equivalent components, which makes it far easier to tell whether an accessibility gap is a genuine framework-specific bug or just a component that hasn’t been built out yet on one side.
Step 7 and 8: Fix Focus Management, Then Wire Up ARIA Live Regions
Step 7: Fix Focus Management and Keyboard Traps
Focus traps in modals are the single most common keyboard-navigation bug axe-core and Playwright scans can’t fully catch on their own, since detecting whether Tab wraps correctly requires simulating actual key presses. React Aria’s useDialog and FocusScope handle this automatically:
// React: modal with automatic focus trapping
import { Modal, Dialog, Heading } from 'react-aria-components'
function ConfirmDialog({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
return (
)
}
On the Vue side, Headless UI’s Dialog component handles the same trap-and-restore behavior, moving focus into the dialog on open and back to the triggering element on close, without any manual tabindex juggling. Manually written modals routinely skip the “restore focus on close” half of this pattern, which is exactly the kind of bug that only surfaces during a real keyboard or screen reader test, not an automated scan.
The same trap-and-restore logic applies to dropdown menus, comboboxes, and any custom widget that overlays content on top of the page, not just modals. If you’re building any of those from scratch instead of reaching for React Aria’s or Headless UI’s pre-built version, budget real time for keyboard testing, because focus bugs in overlay widgets are consistently the hardest category of accessibility issue to catch through automated scanning alone.
Step 8: Implement ARIA Live Regions for Screen Reader Announcements
Form validation errors, toast notifications, and loading states need to be announced to screen reader users even though nothing moved focus. An aria-live region handles that in both frameworks, though the wiring differs slightly because React re-renders declaratively while Vue’s reactivity system needs the region to exist in the DOM before the text inside it changes.
// React: a reusable live-region announcer
function LiveAnnouncer({ message }: { message: string }) {
return (
{message}
)
}
// Vue: same pattern with reactive state
//
//
// {{ announcement }}
//
//
Keep the live region mounted at all times and only change its text content. Unmounting and remounting the element resets it and screen readers won’t reliably pick up the new announcement. Use aria-live="assertive" only for errors that need immediate attention, and default to "polite" for everything else, since assertive announcements interrupt whatever the screen reader is currently reading.
Step 9 and 10: Audit Color Contrast, Then Gate Pull Requests in CI
Step 9: Audit and Fix Color Contrast Issues
Low contrast text is the single most common WCAG failure on the web. WebAIM’s 2026 Million report found it on 83.9% of scanned home pages, up from 79.1% the prior year, ahead of missing alt text (53.1%), unlabeled form fields (51%), and empty links (46.3%). axe-core catches most contrast violations automatically as part of the same Playwright and Storybook scans already running, since contrast ratio is a computed property of rendered text against its background, framework-agnostic by nature.
| WebAIM 2026 Million Failure Type | % of Top 1M Home Pages Affected |
|---|---|
| Low-contrast text | 83.9% |
| Missing alt text on images | 53.1% |
| Unlabeled form fields | 51.0% |
| Empty links | 46.3% |
| Overall detectable WCAG failure rate | 95.9% |
The fix is almost always design-system level, not component-level: set your design tokens (text color, background color pairs) to meet a 4.5:1 ratio for normal text and 3:1 for large text under WCAG 2.2’s 1.4.3 success criterion, then let axe flag any component that overrides those tokens with an inline style. Component libraries that ship dark mode without re-checking contrast in both themes are a frequent source of regressions here.
Step 10: Gate Pull Requests with CI Accessibility Checks
Running axe scans locally only helps if someone remembers to do it. Wire the Playwright accessibility spec into GitHub Actions so a pull request fails the same way a broken unit test would.
# .github/workflows/a11y.yml
name: Accessibility
on: [pull_request]
jobs:
axe-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build && npm run preview &
- run: npx playwright test a11y.spec.ts
Treat this as a release criterion, not a checkbox someone can dismiss. Teams that make the axe scan a required, non-bypassable check catch far more violations before merge than teams that run it as an informational job whose failures get ignored.
Step 11 and 12: Run a Manual Screen Reader Pass and Monitor Production
Step 11: Run a Manual Screen Reader Testing Pass
Automated tools catch roughly 30-40% of WCAG issues. The rest require a human with a screen reader. Test with NVDA + Firefox on Windows and VoiceOver + Safari on macOS, since these are the combinations most guides recommend as the baseline pair. According to WebAIM’s Screen Reader User Survey #10, published in 2024 and still the most recent full dataset available (Survey #11 opened for responses in August 2026 but results aren’t published yet), JAWS leads primary desktop usage at roughly 40.5%, NVDA follows at 37.7%, and VoiceOver sits at 9.7%. Testing with just one screen reader misses real user behavior, since JAWS and NVDA handle some ARIA patterns differently enough that a component passing in one can still fail in the other.
Walk your critical flows (signup, checkout, primary navigation) using only Tab, Shift+Tab, Enter, and arrow keys, with the mouse unplugged if you can manage it. If you get stuck anywhere a sighted mouse user wouldn’t, that’s the bug.
Step 12: Monitor Accessibility in Production
Automated scans in CI catch regressions before merge, but real users find things staging environments never surface, like a third-party embed injecting an unlabeled iframe. Run the same AxeBuilder pattern from Step 5 as a scheduled job against production URLs weekly, and log violations to whatever error tracker your team already uses so accessibility bugs get the same visibility as JavaScript exceptions.
React vs Vue Accessibility: Head-to-Head Comparison
Neither framework is inherently more or less accessible. The DOM either has the right ARIA attributes or it doesn’t, and both frameworks render to the same DOM. Where they genuinely differ is tooling maturity and how much a team has to assemble by hand.
| Category | React | Vue |
|---|---|---|
| Low-level a11y primitives | React Aria (Adobe), Radix Primitives, Ark UI | No direct hook-based equivalent, rely on Headless UI Vue components |
| Pre-built accessible components | react-aria-components 1.21.0 | @headlessui/vue |
| ESLint plugin | eslint-plugin-jsx-a11y 6.10.2 (not yet ESLint 10-compatible) | eslint-plugin-vuejs-accessibility 2.6.0 |
| Runtime dev-mode scanning | @axe-core/react | vue-axe |
| E2E automated testing | @axe-core/playwright (framework-agnostic) | @axe-core/playwright (identical usage) |
| Storybook a11y addon | @storybook/addon-a11y | @storybook/addon-a11y (same package) |
| 2026 WebAIM Million avg. errors/page | 43.5 (22.5% below all-site average) | 64.6 |
The one row worth pausing on is the error-count gap. React’s lower average almost certainly reflects earlier and wider adoption of tools like jsx-a11y and React Aria rather than anything structural about JSX versus Vue’s template syntax. Every tool in the Vue column of that table exists and works. Vue teams that adopt them at the same rate React teams have should expect to close most of that 21-error gap.
Common Accessibility Pitfalls That Slip Through Code Review
- Divs and spans with onClick instead of buttons. A clickable div has no keyboard focus, no Enter/Space activation, and no accessible role unless you add all three manually with
tabindex, a keydown handler, and an explicitrole="button". Reviewers approve this pattern constantly because it looks fine visually and works fine with a mouse, which is exactly why it slips through code review so often. Use a real<button>element and all three problems disappear at once, for free. - Icon-only buttons with no accessible name. A trash-can icon inside a button with no
aria-labelreads as “button” to a screen reader, with zero indication of what it does. Design reviews catch missing icon labels almost never, because a sighted reviewer reads the icon’s meaning instantly and never thinks to check what a screen reader announces instead. - Removing focus outlines globally with
outline: none. This is still one of the most common CSS mistakes flagged in WebAIM’s data, and it breaks keyboard navigation for every sighted keyboard user, not just screen reader users. It usually gets added to fix one ugly default browser outline on a single element, then copy-pasted into a global stylesheet reset where it silently disables focus indicators site-wide. - Modals that trap focus but never restore it. Opening a dialog and moving focus inside it is only half the pattern. Closing it must return focus to whatever element opened it. Skip that half and a keyboard user closes a dialog only to find their focus has silently reset to the top of the page, forcing them to tab through the entire layout again to get back where they were.
- Placeholder text used as the only label. Placeholder text disappears the moment a user starts typing and isn’t reliably announced by every screen reader as a label substitute. It also fails low-vision users who rely on placeholder-colored text being lighter than real content, which often puts it below the contrast threshold covered in Step 9.
- Toast notifications with no live region. A toast that appears and auto-dismisses in three seconds is invisible to a screen reader user unless it’s wrapped in an
aria-liveregion, as covered in Step 8. This is especially common in form-submission flows, where a sighted user sees “Saved successfully” flash on screen while a screen reader user gets no confirmation the save even happened. - Nested interactive elements. Putting a button inside a link, or a link inside a button, creates ambiguous or duplicate focus stops that confuse both keyboard order and screen reader announcement. It’s a pattern that tends to appear when a card component needs to be “clickable” while also containing a separate call-to-action button, and neither React nor Vue’s type systems catch it automatically.
Troubleshooting Common Accessibility Testing Errors
| Symptom | Likely Cause | Fix |
|---|---|---|
| axe-core reports zero violations but a screen reader test still fails | Automated tools only catch ~30-40% of WCAG issues | Run the manual screen reader pass from Step 11, and don’t treat a clean axe scan as full coverage |
| @axe-core/react logs nothing in the console | Import guarded behind import.meta.env.DEV but app built in production mode |
Confirm you’re running npm run dev, not a production preview build |
| eslint-plugin-jsx-a11y throws a config error on install | Project already upgraded to ESLint 10 | Stay on ESLint 9 for this plugin until a compatible release ships, or isolate the a11y lint step in a separate config |
| Playwright’s AxeBuilder times out | Scanning a page with heavy client-side hydration before it’s fully interactive | Add an explicit wait for a post-hydration selector before calling .analyze() |
vue-axe doesn’t detect issues in components rendered via <Teleport> |
Teleported content sometimes renders outside the root node vue-axe watches | Point vue-axe’s config at document.body instead of the app root selector |
| Contrast violations flagged on text that looks fine visually | axe measures computed color against computed background, including semi-transparent overlays | Check for a translucent overlay or gradient sitting behind the text and increase its opacity or add a solid backing color |
| Live region announces the old message, not the new one | The live region element was unmounted and remounted rather than updated in place | Keep the live-region container permanently in the DOM and only change its text node |
| Storybook’s a11y panel shows different results than the CI Playwright scan | Storybook scans an isolated component, the page scan includes surrounding layout and global styles | Treat both as complementary, not duplicate. A component-level pass doesn’t guarantee a page-level pass |
Advanced Tips for Scaling Accessibility Across Large Codebases
Once the base pipeline from Steps 1-12 is running, a few refinements pay off as a codebase grows past a handful of teams. Enforce the axe scan’s WCAG tag list explicitly (wcag2a, wcag2aa, wcag21aa) rather than accepting axe-core’s full default rule set, since some default rules are best-practice suggestions rather than actual WCAG success criteria, and treating every axe warning as a hard CI failure trains teams to suppress rules instead of fixing root causes.
For design systems shared across React and Vue codebases in the same organization, centralize contrast tokens and focus-ring styles in a single design-tokens package both frameworks import, so a contrast fix only needs to happen once. Add axe-core scanning to Chromatic or another visual-regression tool if you’re already running one, since it lets a single pull request comment surface both a visual diff and an accessibility regression side by side. Finally, track the WebAIM Million failure categories (contrast, alt text, labels, empty links) as a quarterly internal dashboard. Teams that measure their own numbers against the industry’s 95.9% failure rate tend to catch regressions faster than teams that only run scans reactively after a complaint.
Large organizations running both React and Vue teams under one umbrella also benefit from a shared accessibility test fixture library, a small internal package containing pre-written Playwright helpers for common patterns (opening a modal, dismissing a toast, tabbing through a form) that both teams import instead of each reinventing them. It sounds like a small thing, but it means a fix to how the test suite detects a focus trap gets applied everywhere at once, rather than drifting apart across two codebases that started from the same tutorial but diverged over eighteen months of independent maintenance. Pair that with a rotating “accessibility champion” role, someone on each team responsible for triaging new axe violations weekly, and most organizations find the backlog stays flat instead of growing quietly in the background until an external audit or lawsuit forces a scramble.
The Complete Working Project
Putting every step together, a fully wired project looks like this on either side:
a11y-react-demo/
├── src/
│ ├── main.tsx # @axe-core/react dev-mode wiring (Step 4)
│ ├── App.tsx
│ └── components/
│ ├── ConfirmDialog.tsx # react-aria-components Modal/Dialog (Step 7)
│ └── LiveAnnouncer.tsx # aria-live region (Step 8)
├── tests/
│ └── a11y.spec.ts # Playwright + AxeBuilder (Step 5)
├── .storybook/
│ └── main.ts # @storybook/addon-a11y (Step 6)
├── eslint.config.js # eslint-plugin-jsx-a11y (Step 3)
└── .github/workflows/a11y.yml # CI gate (Step 10)
a11y-vue-demo/
├── src/
│ ├── main.ts # vue-axe dev-mode wiring (Step 4)
│ ├── App.vue
│ └── components/
│ ├── ConfirmDialog.vue # @headlessui/vue Dialog (Step 7)
│ └── LiveAnnouncer.vue # aria-live region (Step 8)
├── tests/
│ └── a11y.spec.ts # same Playwright spec, different port
├── .storybook/
│ └── main.ts # @storybook/addon-a11y (Step 6)
├── eslint.config.js # eslint-plugin-vuejs-accessibility (Step 3)
└── .github/workflows/a11y.yml # identical CI gate
Both projects share the same Playwright spec file, the same GitHub Actions workflow, and the same Storybook addon. The only pieces that genuinely diverge are the component libraries (React Aria versus Headless UI Vue) and the ESLint plugin, which is a smaller surface area to maintain than most teams expect going in.
Output Examples: What Passing and Failing Scans Look Like
A clean Playwright run against a page with no violations prints a standard passing test result with an empty violations array. A failing run is more useful, since it tells you exactly which rule failed and where:
1) a11y.spec.ts:5:1 › signup page has no WCAG 2.1 AA violations
Error: expect(received).toEqual(expected)
- Expected - 0
+ Received + 1
+ [{
+ "id": "color-contrast",
+ "impact": "serious",
+ "description": "Elements must meet minimum color contrast ratio thresholds",
+ "help": "Elements must meet minimum color contrast ratio thresholds",
+ "nodes": [{ "target": [".submit-button"], "html": "" }]
+ }]
The id field maps directly to an axe-core rule you can look up for the exact fix, and color-contrast here points straight back to the Step 9 contrast audit, and the nodes.target array tells you precisely which CSS selector to fix, so there’s no guesswork about which of a dozen buttons on the page triggered it.
Frequently Asked Questions
Is React or Vue more accessible by default?
Neither. Both render to the same DOM, and WCAG compliance depends on the markup and ARIA attributes your components produce, not the framework rendering them. WebAIM’s 2026 Million data shows React sites averaging fewer errors (43.5 versus Vue’s 64.6), but that reflects how widely each ecosystem has adopted tools like React Aria and jsx-a11y, not a structural difference in how JSX or Vue templates compile down to HTML.
Do I need both automated scans and manual screen reader testing?
Yes. Automated tools like axe-core catch roughly 30-40% of WCAG issues reliably, mostly the ones with an objectively measurable failure state like contrast ratio or a missing attribute. Everything involving actual reading order, meaningful announcements, and real keyboard flow needs a human tester with a screen reader, because those judgments require understanding what the content actually means, not just whether an attribute is present.
Which WCAG version should I target in 2026, 2.1 or 2.2?
Target WCAG 2.2 AA as your build standard. WCAG 2.1 AA remains the version most regulations formally cite as of 2026, including the EU’s EN 301 549 and the US DOJ’s ADA Title II rule, so meeting 2.2 automatically satisfies 2.1 and future-proofs you against regulations that update their references before your next redesign cycle.
Does the European Accessibility Act actually apply to US-based companies?
Yes, if you sell covered digital products or services to EU residents. The EAA has been enforceable since June 28, 2025, and applies extraterritorially regardless of where the company is headquartered, which is a meaningfully different enforcement model than the US, where most action still comes through private lawsuits rather than a proactive market-surveillance authority.
Can I use React Aria components with a Vue project, or vice versa?
No. React Aria and react-aria-components are React-only, built around React’s hooks model, and there’s no supported way to run them inside a Vue template. Headless UI, however, ships separate builds for both React and Vue if you want one library’s design conventions and interaction patterns applied consistently across both stacks.
What’s the fastest single fix for the most common WCAG failure?
Fix color contrast first. It’s the most widespread failure in WebAIM’s 2026 Million data at 83.9% of scanned home pages, well ahead of missing alt text or unlabeled forms, and it’s usually a design-token change (adjusting a handful of text and background color pairs) rather than a component rewrite, which makes it the highest-leverage fix available for the time it takes.
Should accessibility checks block a pull request or just warn?
Block it, once the initial backlog is cleared. Treating axe-core failures as informational warnings that developers can dismiss defeats the purpose of running the check at all. Teams that make the scan a required, non-bypassable CI gate, the same way a failing unit test blocks a merge, catch measurably more violations before they reach production than teams running the same scan as an ignorable status check.
How often should I re-run accessibility scans against production?
Weekly, at minimum, using the same AxeBuilder pattern from the CI step pointed at live URLs instead of a staging server. Third-party embeds, ad units, and CMS-injected content can introduce violations that never appear in a local dev or staging environment, since those environments frequently strip out or mock the exact third-party scripts responsible for the regression.