Accessibility is not a feature you bolt on at the end of a sprint. It is a fundamental quality of software that determines whether real people can actually use what you build. Roughly fifteen percent of the global population lives with some form of disability, and that number grows as populations age. When your navigation trap catches a keyboard user, when your dynamic content silently updates without announcing itself, when your color contrast fails someone in bright sunlight — these are not edge cases. They are everyday failures that exclude real users.
The legal landscape has accelerated this reckoning. The European Accessibility Act took full effect in June 2025, covering all digital products and services sold in the EU. ADA-related web accessibility lawsuits in the United States surpassed 4,600 in 2025. Accessibility is no longer optional for any organization with a web presence. This guide covers the technical foundations you need: WCAG 2.2 conformance, practical ARIA patterns, screen reader testing, and automated testing pipelines that catch regressions before they reach production.
WCAG 2.2: What Changed and What It Means
WCAG 2.2, published as a W3C Recommendation in October 2023, added nine new success criteria to the existing WCAG 2.1 specification. These additions address cognitive accessibility, mobile interaction, and authenticated user flows — gaps that the earlier specification left open.
New Success Criteria That Matter Most
Focus Not Obscured (Minimum) (2.4.11, AA): When a UI component receives keyboard focus, it must not be entirely hidden by author-created content. This targets sticky headers, cookie banners, and chat widgets that sit on top of focused elements. The practical fix is straightforward: ensure sticky elements do not cover the currently focused item, or use scroll-padding-top to create space beneath fixed headers.
Focus Appearance (2.4.13, AAA): The focus indicator must have a minimum area of at least a 2px perimeter around the component and a contrast ratio of at least 3:1 between the focused and unfocused states. Most custom focus styles already meet this, but if you have removed outlines without replacing them with visible indicators — a depressingly common practice — this criterion requires you to fix that immediately.
Dragging Movements (2.5.7, AA): Any action that uses dragging must have a single-pointer alternative. Drag-and-drop reordering must also support button-based up/down controls. This affects kanban boards, sortable lists, and file upload zones.
Target Size (Minimum) (2.5.8, AA): Interactive targets must be at least 24 by 24 CSS pixels, or have sufficient spacing from adjacent targets. Inline links within text are exempt. This criterion finally gives teams a concrete number to design against, ending debates about whether 20px touch targets are "close enough."
Accessible Authentication (Minimum) (3.3.8, AA): Authentication flows must not rely on cognitive function tests — memorizing passwords, transcribing CAPTCHAs, solving puzzles — unless an alternative method is available. Support for password managers, passkeys, and email-based magic links satisfies this criterion.
Conformance Levels in Practice
Most organizations target WCAG 2.2 Level AA, which includes all A and AA criteria. Level AAA contains additional criteria that benefit users but are often impractical to apply across an entire site (for example, requiring a reading level assessment of all content). The practical approach: conform to AA across your entire product, and apply select AAA criteria — like Focus Appearance and Enhanced Contrast — where feasible.
ARIA Patterns Done Right
ARIA (Accessible Rich Internet Applications) extends HTML semantics to describe dynamic UI patterns that native HTML elements cannot express. The first rule of ARIA remains "do not use ARIA if you can use a native HTML element." But modern interfaces routinely include tabs, dialogs, comboboxes, and live-updating regions that require ARIA to be accessible.
Landmark Roles
Landmarks give screen reader users a way to navigate the high-level structure of your page. Use native HTML5 elements where possible — <header>, <nav>, <main>, <aside>, <footer> — which carry implicit ARIA roles. When you have multiple navigation regions, differentiate them with aria-label:
<nav aria-label="Primary">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/products">Products</a></li>
</ul>
</nav>
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li aria-current="page">Products</li>
</ol>
</nav>
<main>
<!-- Page content -->
</main>
<aside aria-label="Related articles">
<!-- Sidebar content -->
</aside>
Tab Panels
The tabs pattern requires coordinated roles and keyboard handling. Each tab gets role="tab" inside a role="tablist". The associated panel gets role="tabpanel". Arrow keys move between tabs, and only the active tab sits in the tab order.
<div role="tablist" aria-label="Account settings">
<button role="tab"
aria-selected="true"
aria-controls="panel-profile"
id="tab-profile"
tabindex="0">
Profile
</button>
<button role="tab"
aria-selected="false"
aria-controls="panel-security"
id="tab-security"
tabindex="-1">
Security
</button>
<button role="tab"
aria-selected="false"
aria-controls="panel-notifications"
id="tab-notifications"
tabindex="-1">
Notifications
</button>
</div>
<div role="tabpanel"
id="panel-profile"
aria-labelledby="tab-profile"
tabindex="0">
<!-- Profile form content -->
</div>
<div role="tabpanel"
id="panel-security"
aria-labelledby="tab-security"
tabindex="0"
hidden>
<!-- Security form content -->
</div>
Live Regions
Dynamic content updates — toast notifications, form validation messages, chat messages, real-time data — need aria-live to announce changes to screen readers. Use aria-live="polite" for non-urgent updates (search result counts, status messages) and aria-live="assertive" sparingly for critical alerts (error messages, session timeouts). The region must exist in the DOM before content is injected into it; adding both the region and its content simultaneously often causes the announcement to be missed.
Dialog and Modal Patterns
Modals are among the most commonly broken accessibility patterns. A properly accessible dialog requires focus trapping (Tab cycles within the dialog), an Escape key handler to close it, a return of focus to the triggering element on close, and aria-modal="true" with role="dialog". The native <dialog> element, now supported across all major browsers, handles most of these behaviors automatically and should be your default choice in 2026.
Screen Reader Testing Workflows
Automated testing catches roughly 30 to 40 percent of accessibility issues. The rest require manual testing, and the most valuable manual testing involves screen readers. Each screen reader behaves differently, so testing with a single tool is insufficient.
| Screen Reader | Platform | Browser Pairing | Market Share (2026) | Best For |
|---|---|---|---|---|
| JAWS | Windows | Chrome, Edge | ~40% | Enterprise, power users |
| NVDA | Windows | Firefox, Chrome | ~31% | Free option, developer testing |
| VoiceOver | macOS / iOS | Safari | ~20% | Mobile testing, Apple ecosystem |
| TalkBack | Android | Chrome | ~7% | Android mobile testing |
NVDA with Firefox is the recommended starting point for developer testing. NVDA is free, runs on Windows (including virtual machines for macOS users), and Firefox's accessibility implementation is consistently reliable. Learn the basics: Insert+Down Arrow to start reading, Tab to move through interactive elements, H to jump between headings, D to jump between landmarks.
VoiceOver on macOS is built in and requires no installation. Activate it with Command+F5. Use VO+Right Arrow (where VO is Ctrl+Option) to move through content. VoiceOver's rotor (VO+U) provides quick navigation by headings, links, form controls, and landmarks — use it to verify your page structure makes sense when consumed linearly.
Testing workflow: For each page or component, test five things. First, navigate using only the Tab key and verify every interactive element is reachable and has a visible focus indicator. Second, activate every control using Enter or Space. Third, use the screen reader's heading navigation to verify the heading hierarchy is logical. Fourth, trigger every dynamic update (form validation, loading states, notifications) and confirm announcements occur. Fifth, test any custom widget (tabs, menus, dialogs) with the expected keyboard pattern from the ARIA Authoring Practices Guide.
Automated Accessibility Testing
Manual testing is essential but does not scale. Automated tools form the first line of defense, catching structural issues like missing alt text, empty buttons, broken label associations, and insufficient contrast before code reaches production.
axe-core: The Industry Standard
axe-core by Deque Systems is the most widely used accessibility testing engine. It powers the browser extensions (axe DevTools), integrates with test frameworks, and runs in CI pipelines. Its rule set maps directly to WCAG success criteria, and its zero-false-positive guarantee for its "violation" severity means you can trust its findings without manual verification.
Integration with Playwright provides the most comprehensive automated testing setup in 2026:
// accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility compliance', () => {
test('homepage has no violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('checkout flow is accessible', async ({ page }) => {
await page.goto('/checkout');
// Fill in shipping form
await page.getByLabel('Full name').fill('Test User');
await page.getByLabel('Email').fill('test@example.com');
// Scan after interaction to catch dynamic content issues
const results = await new AxeBuilder({ page })
.include('.checkout-form')
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('modal dialog maintains focus', async ({ page }) => {
await page.goto('/products');
await page.getByRole('button', { name: 'Quick view' }).first().click();
// Verify dialog received focus
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
// Run axe on the dialog specifically
const results = await new AxeBuilder({ page })
.include('[role="dialog"]')
.analyze();
expect(results.violations).toEqual([]);
});
});
CI Pipeline Integration
Accessibility testing belongs in your continuous integration pipeline alongside unit tests and linting. The most effective approach uses multiple layers. Run axe-core via Playwright on every pull request against key user flows. Run Lighthouse accessibility audits in CI to track your score over time. Use eslint-plugin-jsx-a11y (for React) or equivalent linting rules to catch issues at the authoring stage. Run Pa11y against your sitemap for broad coverage of all public pages.
The critical principle: treat accessibility violations as build failures. If your CI pipeline allows accessibility regressions to pass with a warning, they will accumulate. Set a threshold of zero violations for axe-core rules at the "violation" severity, and address "needs review" items in periodic manual audits.
Common Accessibility Failures and Fixes
After auditing hundreds of web applications, certain patterns appear again and again. Fixing these covers the vast majority of issues users actually encounter.
Color Contrast
WCAG requires a minimum contrast ratio of 4.5:1 for normal text and 3:1 for large text (18px bold or 24px regular). Light gray text on white backgrounds is the single most common accessibility failure across the web. Use a contrast checker during design, not after development. The CSS color-contrast() function, now supported in modern browsers, can select the best text color automatically from a list of options based on the background.
Missing Form Labels
Every form input needs an accessible name. The most reliable method is a visible <label> element associated via the for attribute. Placeholder text is not a label — it disappears on input and has insufficient contrast in most browsers. For inputs where a visible label is impractical (search fields with a magnifying glass icon), use aria-label as a fallback.
Focus Management
When content changes dynamically — a form step advances, a modal opens, a single-page app navigates — focus must move to the appropriate location. If a user activates a button that reveals new content, focus should move to that content or its heading. When a modal closes, focus returns to the element that opened it. Without explicit focus management, keyboard and screen reader users are stranded at the top of the page or lost in the DOM after every interaction.
Keyboard Navigation
Every feature accessible by mouse must be accessible by keyboard. This means every clickable element must be focusable (use <button> and <a> elements, not click handlers on <div> or <span>). Custom components need appropriate keyboard handling: Enter and Space to activate buttons, Arrow keys to navigate within composite widgets, Escape to dismiss overlays. Test by unplugging your mouse for an hour and using your application. The friction you discover is what keyboard users experience permanently.
Image Alt Text
Every <img> element needs an alt attribute. Decorative images get an empty alt="" so screen readers skip them. Informative images get alt text that conveys the same information visually sighted users receive — not "image of chart" but "Bar chart showing revenue growth of 23% from Q1 to Q3 2026." Complex images like charts and diagrams should supplement alt text with a longer description via aria-describedby or a visible caption.
Building an Accessibility Culture
Tools and standards matter, but accessibility ultimately depends on people. Organizations that treat accessibility as a compliance checkbox produce barely-passing interfaces. Organizations that build accessibility into their culture produce genuinely inclusive products.
Shift-Left Testing
The most expensive accessibility bug is one found after launch. Move accessibility consideration as early as possible in your development process. Designers should annotate mockups with heading levels, focus order, and alt text. User stories should include acceptance criteria for keyboard navigation and screen reader behavior. Code reviews should include an accessibility checklist. The earlier you catch issues, the cheaper they are to fix — a missing heading structure caught in design review costs minutes, while the same issue found in a production audit costs hours of refactoring.
Developer Training
Most accessibility defects stem from lack of awareness, not lack of skill. Invest in training that goes beyond rules and into empathy. Have developers use screen readers for an afternoon. Have them navigate your product with a keyboard only. These experiences create lasting understanding that no checklist can replicate. Pair experienced accessibility practitioners with feature teams during sprint planning, not just during audits.
Governance and Standards
Establish an accessibility standard for your organization — typically WCAG 2.2 Level AA — and make it a shipping requirement equal to security and performance. Create a component library with accessibility built in so individual teams cannot accidentally introduce patterns that exclude users. Document your accessible component patterns and make them the path of least resistance. When the accessible option is also the easiest option, adoption follows naturally.
Accessibility is not a destination you reach. It is a practice you maintain. Every deployment, every new feature, every design change is an opportunity to include or exclude. Choose inclusion deliberately, measure it continuously, and defend it structurally.
The tooling has never been better. axe-core catches structural issues automatically. WCAG 2.2 provides clear, testable criteria. Screen readers are free and built into every operating system. The remaining barrier is not technical — it is cultural. Build accessibility into how your team works, and the technical implementation follows. Your users deserve nothing less.