PrimeStack.
Engineering·Sep 15, 2025

Accessible by Default: Building Inclusive Web Apps

Accessibility shouldn't be an afterthought. Learn how to bake a11y into your development process with automated testing.


Accessibility is an engineering responsibility. The moment you ship a button that can't be reached by keyboard, a form that doesn't announce errors to screen reader users, or a color scheme that fails contrast requirements, you've introduced a bug — just one that only certain users will encounter. Building accessible web apps means treating WCAG compliance the same way you treat type safety or test coverage: a baseline quality standard, not an optional enhancement.

This article is a practical, opinionated guide to building accessibility into your development process from the start — not retrofitting it at the end of a sprint when it's expensive and painful.

Table of Contents


Why Accessibility Is an Engineering Problem

Design teams set the visual direction, but engineers decide what HTML elements get rendered, whether focus states exist, and whether interactive widgets are keyboard-operable. A perfectly designed accessible component can be implemented in a way that makes it completely unusable by assistive technology. The inverse is also true: good engineering can compensate for incomplete design specs.

The misconception that accessibility is purely a design concern leads to it being dropped entirely when timelines get tight. Engineers who understand accessibility at the code level are the last line of defense before inaccessible code ships. That means knowing the WCAG criteria, understanding how screen readers interpret the DOM, and having automated checks that surface issues before they reach review.

Accessibility is also a legal exposure. The number of web accessibility lawsuits filed under the ADA in the United States has increased every year since 2018. In 2023, over 4,600 federal lawsuits were filed — more than 12 per day. The EU's European Accessibility Act requires compliance for many digital products by June 2025. Ignorance is not a defense.


The WCAG 2.2 Framework

WCAG 2.2, published by the W3C, organizes accessibility requirements around four principles. Every success criterion maps to one of them.

Perceivable

Information and UI components must be presentable to users in ways they can perceive. This means providing text alternatives for non-text content (alt attributes on images, captions for video), ensuring content doesn't rely on color alone to convey meaning, and meeting contrast ratios. Users who are blind, deaf, or have low vision must be able to access the same content.

Operable

All functionality must be operable through a keyboard. This includes navigation, form submission, modal interactions, drag-and-drop, and any custom widget. Users must also have enough time to read and use content — no content that moves or blinks without pause controls, and no timing requirements that can't be adjusted.

Understandable

The UI must be predictable and the content legible. This means language is declared on the page (lang attribute), error messages are specific and actionable, and components behave consistently across the application. A button that submits a form in one context shouldn't behave differently without explanation.

Robust

Content must be interpreted reliably by current and future user agents, including assistive technologies. Valid HTML, correct ARIA usage, and using standard web platform primitives over custom implementations all contribute here.

WCAG 2.2 adds several new criteria over 2.1, including focus appearance requirements (2.5.11, 2.5.12), target size minimums (2.5.8), and accessibility of authentication processes (3.3.7, 3.3.8). If your target compliance level is AA, all of these apply.


Semantic HTML: The Unmovable Foundation

Before you reach for ARIA, ask whether native HTML already does the job. It almost always does.

A <button> is focusable by default, fires on Enter and Space, has an implicit role="button", and is announced as a button by every screen reader. A <div> with onClick is none of those things. You'd need tabindex="0", role="button", onKeyDown handling for Enter and Space, and you'd still be fighting browser inconsistencies. The right element does this automatically.

The same principle applies throughout:

  • Use <nav> for navigation landmarks, <main> for primary content, <aside> for supplementary content, <header> and <footer> for their respective regions. Screen readers expose these as landmarks, letting users jump between them.
  • Use <ul>/<ol> for lists. VoiceOver on macOS announces list semantics, and that context matters when a user is scanning a page.
  • Use <table> with <thead>, <th scope="col">, and <caption> for tabular data — not for layout.
  • Use <h1><h6> in a logical hierarchy. Screen reader users navigate by headings more than any other landmark.

The cost of a <div> soup is invisible until you use a screen reader. Then it's catastrophic.


ARIA: When to Use It and When to Step Away

ARIA (Accessible Rich Internet Applications) lets you add accessibility semantics to HTML that otherwise has none. The canonical example is a custom dropdown built from <div> elements: without ARIA, it's invisible to assistive technology. With role="combobox", aria-expanded, aria-activedescendant, and the correct keyboard interaction pattern from the ARIA Authoring Practices Guide, it can be made accessible.

The first rule of ARIA is: don't use ARIA if native HTML will do. The second rule is: don't change native semantics unless you have a very good reason.

When ARIA Makes Things Worse

Adding role="button" to an <a> element that navigates to a URL breaks expected behavior for screen reader users who distinguish between links and buttons. Adding aria-hidden="true" to an element that contains interactive controls removes those controls from the accessibility tree — the user can't reach them at all. Setting aria-label on a container while its visible text says something different creates a mismatch that confuses users.

Common ARIA mistakes that break accessibility:

  • aria-hidden on focusable children
  • aria-label that duplicates or contradicts visible text
  • role="presentation" on elements with meaningful semantics
  • Missing required owned elements (e.g., role="listbox" without role="option" children)
  • Live regions (aria-live) with incorrect politeness settings that either spam the user or stay silent

Use the ARIA Authoring Practices Guide (APG) at aria.w3.org as your authoritative reference for widget patterns. Don't invent keyboard interactions — follow the established patterns for accordions, dialogs, tabs, and trees.


Keyboard Navigation Patterns

Every interactive element must be reachable and operable by keyboard. Start by testing your application with Tab, Shift+Tab, Enter, Space, and arrow keys. If you can't reach something, your keyboard users can't either.

Focus Management

When content changes dynamically — a modal opens, a route changes, an inline form appears — focus must be explicitly managed. If a modal opens and focus stays on the trigger button, a screen reader user has no idea the modal exists. Move focus to the modal's heading or first interactive element when it opens. Return focus to the trigger when it closes.

Use element.focus() to manage focus programmatically. Be careful with tabindex: tabindex="0" adds an element to the natural tab order, tabindex="-1" makes it programmatically focusable without putting it in the tab sequence. Positive tabindex values (tabindex="1", etc.) create a separate tab order that's nearly always wrong — avoid them.

Focus Traps in Modals

When a modal dialog is open, keyboard focus must be trapped inside it. A user tabbing through a modal should cycle through the modal's interactive elements, not escape to the background content. The background should also have aria-hidden="true" applied while the modal is open.

Implementing a focus trap: collect all focusable elements inside the modal container, intercept Tab and Shift+Tab keydown events, and manually wrap focus when the user reaches the first or last element. Libraries like focus-trap handle this correctly and are worth using rather than rolling your own.

A skip link is an anchor at the top of the page that lets keyboard users jump past navigation to the main content. It's the first focusable element on the page and typically visually hidden until focused. Every page needs one.

<a href="#main-content" class="skip-link">Skip to main content</a>

The target needs a matching id and, if it's not naturally focusable, tabindex="-1" so it can receive programmatic focus when the link is activated.


Color Contrast Requirements and Tools

WCAG 2.2 AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (18pt or 14pt bold) and UI components. AAA requires 7:1 for normal text.

Tools for checking contrast:

  • WebAIM Contrast Checker — input hex values, get ratios instantly
  • Colour Contrast Analyser (desktop app) — eyedropper for checking contrast directly on your screen
  • browser DevTools — Chrome and Firefox both show contrast ratios in the color picker when inspecting text
  • axe DevTools browser extension — flags failing contrast in context

When using OKLCH or other modern color spaces, be aware that tooling support for calculating contrast in those spaces varies. APCA (Advanced Perceptual Contrast Algorithm) is a proposed replacement for the WCAG contrast formula that's more perceptually accurate, but it's not yet part of any WCAG standard.

Don't use color alone to convey state. A red border on an invalid input is invisible to colorblind users without a supporting icon or text label.


Accessible Forms

Forms are where the most accessibility failures occur and where the impact on users is highest. A form that can't be completed is a wall.

Label Association

Every input needs a programmatically associated label. The explicit method — <label for="id"> paired with <input id="id"> — is the most reliable. Wrapping the input inside the label also works but is less universally supported. Placeholder text is not a label substitute: it disappears on focus, fails contrast requirements, and is not announced consistently by screen readers.

Error Announcements

When a form validation error occurs, users must know it happened and understand what to fix. Move focus to the first error, or to an error summary above the form that lists all errors. Associate individual error messages with their inputs using aria-describedby. Use aria-invalid="true" on invalid inputs.

<input
  id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid="true"
/>
<span id="email-error" role="alert">Enter a valid email address.</span>

The role="alert" causes the message to be announced immediately by screen readers when it appears in the DOM.

Fieldset and Legend

Group related inputs — especially radio buttons and checkboxes — inside <fieldset> with a <legend>. The legend provides the group question or label, which screen readers announce before each individual option. Without it, a user hears "Yes" with no context about what they're saying yes to.


Screen Reader Testing Workflow

Automated tools catch roughly 30–40% of accessibility issues. The rest require manual testing with actual assistive technology.

NVDA on Windows

NVDA (NonVisual Desktop Access) is free and widely used. Pair it with Firefox for the most representative Windows screen reader experience. Learn the core browse mode vs. forms mode distinction — screen readers switch between them based on context, and understanding this explains a lot of unexpected behavior.

VoiceOver on Mac and iOS

VoiceOver is built into macOS and iOS. On macOS, test with Safari — the WebKit accessibility tree implementation differs from Chromium. Use the VoiceOver rotor (VO + U) to navigate by headings, links, and form controls. On iOS, swipe navigation tests touch-based screen reader usage, which has different interaction patterns from desktop.

TalkBack on Android

TalkBack is the built-in screen reader on Android. It uses swipe gestures to move between elements and double-tap to activate. Test your mobile web experience with TalkBack in Chrome. Pay particular attention to focus order, which on touch devices is determined by DOM order rather than visual layout.

Cross-test critical user flows — authentication, checkout, primary navigation — across at least two screen reader/browser combinations before shipping.


Automated Accessibility Testing

axe-core

axe-core is the industry-standard accessibility rules engine. It runs in the browser and surfaces violations with clear descriptions, impact levels, and remediation guidance. Integrate it via the axe DevTools browser extension for exploratory testing or via the axe-core npm package for programmatic use.

jest-axe

For component-level testing in a Jest + jsdom environment:

import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('Button has no accessibility violations', async () => {
  const { container } = render(<Button>Submit</Button>);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

This catches markup-level issues at the component test layer, before the component is ever rendered in a browser.

Playwright Accessibility Checks

Playwright's accessibility snapshot and @axe-core/playwright integration enable end-to-end accessibility checks against real browser rendering:

import AxeBuilder from '@axe-core/playwright';

test('homepage has no critical accessibility violations', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});

This catches context-dependent issues that component tests miss: contrast failures, landmark structure problems, missing page titles.


Integrating Accessibility into CI/CD

Automated accessibility checks belong in your pipeline, not just in local development.

Add jest-axe assertions to your component test suite. These run on every PR automatically. Add Playwright accessibility checks to your E2E suite targeting critical flows. Configure your CI to fail builds when violations are introduced — don't allow them to be warnings that accumulate.

Use a tool like pa11y-ci to run scheduled sweeps across your entire sitemap, catching regressions that slip through component-level testing. Configure it with a threshold that distinguishes between violations (fail the build) and best-practice notices (report but don't fail).

Track accessibility debt the same way you track other technical debt: in your issue tracker, with severity labels, and with a plan to address it. Don't let "we'll fix it later" become "it's been in production for two years."


The Business Case for Accessibility

The disability community represents roughly 1.3 billion people worldwide — around 16% of the global population, according to the WHO. In the US, people with disabilities have an estimated $490 billion in disposable income. An inaccessible product excludes a market that's larger than most target demographics.

Accessibility also improves experience for everyone. High-contrast modes help users in bright sunlight. Keyboard navigation benefits power users. Clear error messages help all users, not just those using assistive technology. Captions serve users in noisy environments. This is the curb-cut effect: features built for people with disabilities improve the experience for everyone.

From an SEO standpoint, semantic HTML, descriptive alt text, logical heading hierarchy, and page structure are practices that both screen readers and search engine crawlers depend on. Accessibility and SEO share a common foundation.

The legal risk is real and growing. The safest legal position is full WCAG 2.2 AA compliance. The practical position is documented good-faith effort: automated testing in CI, manual testing workflows, a process for addressing reported issues. Companies that have no accessibility process and ignore user complaints face the highest exposure.

Accessibility is not a feature you add. It's a quality standard you maintain. The earlier it's built into your process, the cheaper and more effective it is. By the time a component is in production and a lawsuit is filed, you're paying ten times what it would have cost to do it right.

Start with semantic HTML. Add automated testing to your pipeline this week. Build the habit before you need to fix the damage.