Building Accessible Web Applications: Beyond Checkbox Compliance
Most teams treat accessibility like a tax audit. Something unpleasant that happens once a year, resolved by running an automated scanner and fixing whatever it flags. The result is software that technically passes a checklist while remaining fundamentally unusable for the people those checklists were designed to protect.
Accessible web applications development is not a QA task. It is an engineering discipline — one that requires the same rigor, design thinking, and architectural commitment as performance optimization or security hardening. The difference is that when you get security wrong, your data leaks. When you get accessibility wrong, real people are locked out of your product entirely.
The gap between checkbox compliance and genuine usability is enormous, and closing it demands more than a plugin or a CI step. It demands a shift in how we think about the web.
Accessibility Is an Engineering Problem
The framing matters. When accessibility is treated as a compliance obligation — something legal tells engineering to handle — the outcome is predictable: a shallow pass over color contrast ratios, some alt text sprinkled onto images, and a report filed away. When it is treated as an engineering constraint, the way responsive design or internationalization are, it shapes architecture from the ground up.
Consider how differently a team approaches performance. Nobody waits until the end of a sprint to “add performance.” Teams choose efficient data structures, lazy-load assets, and profile render cycles throughout development. Accessibility deserves the same treatment. The DOM structure you choose on day one determines whether a screen reader can parse your interface six months later.
The organizations doing this well — GOV.UK, the BBC, Shopify — embed accessibility into their component libraries and design systems. It is not a feature. It is a property of the system.
WCAG 2.2: What Actually Changed
WCAG 2.2, finalized in late 2023, introduced nine new success criteria. Three of them deserve particular attention from frontend engineers:
- 2.4.11 Focus Not Obscured (Minimum) — AA: When an element receives keyboard focus, it must not be entirely hidden by author-created content like sticky headers or cookie banners. This is surprisingly easy to violate with fixed-position navigation.
- 2.5.7 Dragging Movements — AA: Any functionality that uses dragging must also offer a single-pointer alternative. Your Kanban board needs a non-drag way to move cards.
- 2.5.8 Target Size (Minimum) — AA: Interactive targets must be at least 24×24 CSS pixels, with exceptions for inline links and elements where the browser controls sizing. This one catches more components than teams expect.
The full specification is dense, but these three criteria reflect a broader shift: WCAG is increasingly concerned with motor and cognitive accessibility, not just visual impairment. Your designs need to account for users who cannot perform precise movements or drag gestures.
Semantic HTML: The Foundation You Are Probably Ignoring
Every accessibility conversation should start with semantic HTML, because it solves more problems than most developers realize. A properly structured document — using <nav>, <main>, <article>, <aside>, headings in logical order — gives assistive technology a complete map of your page without a single line of ARIA.
<!-- Poor structure: a screen reader sees a flat list of divs -->
<div class="nav">...</div>
<div class="content">
<div class="title">Dashboard</div>
<div class="card">...</div>
</div>
<!-- Proper structure: landmarks and heading hierarchy -->
<nav aria-label="Main navigation">...</nav>
<main>
<h1>Dashboard</h1>
<section aria-labelledby="stats-heading">
<h2 id="stats-heading">Usage Statistics</h2>
...
</section>
</main>
The second example is not just better for screen readers. It is better for SEO, better for readability, and better for future maintainability. Native HTML elements carry implicit ARIA roles — <nav> is automatically role="navigation", <button> is automatically role="button" with built-in keyboard handling. Reach for these first.
ARIA: A Repair Tool, Not a Feature
The first rule of ARIA, as stated in the official specification, is: do not use ARIA if you can use a native HTML element instead. ARIA exists to bridge gaps where HTML falls short — custom widgets, dynamic content regions, complex interactive patterns. It does not exist to paper over <div> soup.
When ARIA is necessary, use it precisely:
<!-- Custom dropdown: ARIA is justified here -->
<div role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-controls="options-list"
aria-label="Select a country">
<input type="text" aria-autocomplete="list" />
</div>
<ul id="options-list" role="listbox" aria-label="Countries">
<li role="option" aria-selected="false">Canada</li>
<li role="option" aria-selected="true">Germany</li>
</ul>
The danger of ARIA is that incorrect usage actively harms accessibility. A <div role="button"> without keyboard event handlers is worse than a plain <div>, because it tells assistive technology the element is interactive when it is not. ARIA makes promises to the accessibility tree. Your code must keep those promises.
Keyboard Navigation: The Overlooked Interaction Model
If your application cannot be operated entirely with a keyboard, it is not accessible. Full stop. Keyboard navigation is not an edge case — it is the primary interaction model for screen reader users, many motor-impaired users, and a surprising number of power users who simply prefer it.
Key patterns to implement correctly:
| Pattern | Expected Behavior |
|---|---|
| Tab / Shift+Tab | Move between interactive elements in logical order |
| Enter / Space | Activate buttons, links, and controls |
| Arrow keys | Navigate within composite widgets (tabs, menus, radio groups) |
| Escape | Close modals, dropdowns, and overlays; return focus to trigger |
| Home / End | Jump to first/last item in a list or menu |
Focus management is where most applications break down. When a modal opens, focus must move into it. When it closes, focus must return to the element that triggered it. When content is dynamically loaded, focus should not jump unpredictably. These are engineering requirements, not nice-to-haves.
// Focus trap for a modal dialog
function trapFocus(modalElement) {
const focusable = modalElement.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modalElement.addEventListener('keydown', (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
first.focus();
}
Color Contrast and Visual Design
WCAG AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (18px bold or 24px regular). These are minimums. Aim higher when possible — particularly for body text, where readability at various screen qualities matters.
Common failures that automated tools often miss:
- Text over images: A contrast ratio measured against a solid background is meaningless if the text overlays a photograph. Use a semi-transparent backdrop or ensure the image area beneath text has sufficient contrast.
- Disabled states: Grayed-out buttons with 2:1 contrast ratios are technically exempt from WCAG, but they are still a usability problem. Make disabled states distinguishable through more than just color — use patterns, borders, or icons.
- Focus indicators: The default browser focus ring is ugly, so teams remove it. Then they forget to add a replacement. A visible focus indicator is mandatory for keyboard users. Style it, but never remove it.
- Information conveyed by color alone: Red/green status indicators, required field markers that only use color — these fail for the roughly 8% of men with color vision deficiency. Always pair color with text, icons, or patterns.
Screen Reader Testing: NVDA and VoiceOver
Automated tools catch roughly 30-40% of accessibility issues. The rest require manual testing with actual assistive technology. Two screen readers should be part of every frontend developer’s testing workflow:
NVDA (Windows, free) pairs with Firefox or Chrome and represents a large segment of screen reader users. VoiceOver (macOS/iOS, built-in) is essential for testing Safari and mobile accessibility. Their behavior differs in meaningful ways — how they announce ARIA live regions, how they handle dynamic content, how they navigate tables.
A practical testing routine:
- Navigate the entire page using only the Tab key. Can you reach every interactive element? Is the order logical?
- Use the screen reader’s heading navigation (NVDA:
Hkey; VoiceOver:VO+Command+H) to jump between headings. Does the hierarchy make sense? - Activate a modal or dynamic content change. Is the update announced? Does focus move correctly?
- Complete a form from start to finish. Are labels announced? Are error messages associated with their fields?
- Test on mobile. VoiceOver on iOS with swipe gestures reveals entirely different problems than desktop testing.
This is not optional testing. If you have never used a screen reader to navigate your own application, you do not know whether it works.
Automated Testing: axe and Lighthouse
Automated testing catches the low-hanging fruit — missing alt text, broken label associations, insufficient contrast. Two tools dominate the space:
axe-core by Deque is the engine behind most accessibility testing integrations. It can run in the browser (via the axe DevTools extension), in CI (via @axe-core/cli or jest-axe), and as part of end-to-end test suites.
// Integration with Jest and React Testing Library
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('LoginForm has no accessibility violations', async () => {
const { container } = render(<LoginForm />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
Lighthouse includes an accessibility audit that overlaps partially with axe. It is useful for quick checks but should not be your only tool — its scoring system can give a false sense of completeness. A Lighthouse accessibility score of 100 does not mean your application is accessible. It means it passed the specific automated checks that Lighthouse runs.
The ideal setup: axe in your component test suite to catch regressions early, plus manual screen reader testing on every feature branch that touches UI.
React and Vue: Framework-Specific Pitfalls
Component-based frameworks introduce accessibility problems that do not exist in static HTML.
React pitfalls:
- Fragments (
<>...</>) can break heading hierarchy if components are composed carelessly. ACardcomponent that always renders an<h3>will produce incorrect structure when placed inside a section that expects an<h4>. - Client-side routing does not announce page changes. When using React Router, you need to manually manage focus and announce navigation to screen readers using a live region or focus management.
- Portals (used for modals and tooltips) break DOM order. Focus management and ARIA relationships must be explicitly maintained.
Vue pitfalls:
- Transition components can leave screen readers in limbo during animations. Ensure
aria-hiddenis toggled correctly during enter/leave transitions. v-htmlbypasses template compilation and can inject inaccessible markup. Sanitize and validate any dynamic HTML for proper structure.- Dynamic component loading (
<component :is="...">) can cause focus loss when components swap. Track and restore focus after dynamic updates.
Form Accessibility: Where Most Applications Fail
Forms are the highest-stakes accessibility surface in most applications. Users are submitting data, making purchases, creating accounts. A form that a screen reader cannot parse is not just inconvenient — it is a barrier to core functionality.
<!-- Accessible form pattern -->
<form aria-labelledby="form-title" novalidate>
<h2 id="form-title">Create Account</h2>
<div>
<label for="email">Email address <span aria-hidden="true">*</span></label>
<input id="email" type="email" required
aria-required="true"
aria-describedby="email-hint email-error"
aria-invalid="false" />
<p id="email-hint">We will send a confirmation link.</p>
<p id="email-error" role="alert" hidden>
Please enter a valid email address.
</p>
</div>
</form>
The critical details: every input needs a visible <label> linked via for/id. Error messages must be programmatically associated with their fields using aria-describedby. Required fields need aria-required="true", not just a visual asterisk. Validation errors should use aria-invalid and role="alert" to ensure they are announced immediately.
Placeholder text is not a label. It disappears on input, has insufficient contrast by default, and is not reliably announced by all screen readers. Use it as supplemental hint text at most.
The Business Case Beyond Compliance
The compliance argument is obvious: lawsuits are expensive. ADA-related web accessibility lawsuits in the US exceeded 4,000 filings in recent years, and the European Accessibility Act applies mandatory requirements starting June 2025. But the compliance framing is reductive. It positions accessibility as risk mitigation rather than value creation.
The stronger arguments:
- Market size: Over one billion people globally live with some form of disability. In the US alone, the disability community controls over $490 billion in disposable income. An inaccessible product excludes these customers.
- SEO overlap: Semantic HTML, proper heading hierarchies, descriptive link text, and image alt text are accessibility requirements that directly improve search engine indexing.
- Mobile and situational use: Accessibility improvements benefit everyone. Captions help users in noisy environments. High contrast helps users in bright sunlight. Keyboard navigation helps users with a broken trackpad. The curb-cut effect is real.
- Code quality: Accessible code is well-structured code. Teams that take accessibility seriously produce more maintainable, better-documented, more testable components. The discipline required to build accessible interfaces raises the overall quality bar.
Moving Forward
Accessible web applications development is not about perfection. It is about commitment to a practice — the same way security is a practice, not a state you achieve once and forget. Start with semantic HTML. Add automated testing to your CI pipeline. Spend thirty minutes with a screen reader on your own product. Fix what you find. Repeat.
The gap between passing an automated audit and building something that a blind user, a motor-impaired user, or a cognitively disabled user can actually operate is vast. Closing that gap is engineering work. Hard, detailed, important engineering work. And it is long overdue in our industry.