For most of the web's history, accessibility lived in the "nice to have" column: something teams promised to get to after launch, right next to the redesigned 404 page. That era is over. Since June 28, 2025, the European Accessibility Act has been enforceable across the EU — and it reaches well beyond European companies. In the US, ADA web lawsuits keep climbing, with e-commerce the favorite target. The question has shifted from whether accessibility applies to your product to how much it will cost you to keep ignoring it.
But here's the thing we keep telling clients: the legal argument is the weakest reason to do this work. Accessibility is a proxy for engineering quality. A codebase that handles keyboard focus correctly, names its controls, and respects user preferences is almost always well-structured everywhere else too. So this post covers both halves: what the law actually requires, and how we build for it as a matter of craft — with real code.
One note first: we're engineers, not lawyers, and nothing here is legal advice. If you have real compliance exposure, get counsel who knows your jurisdictions.

The regulatory landscape
The European Accessibility Act
The EAA (Directive (EU) 2019/882) has applied accessibility rules to a defined set of products and services across all member states since June 28, 2025. The parts that matter most for web teams:
- Scope. The EAA covers products and services considered essential to daily life: e-commerce, consumer banking and financial services, electronic communications, e-books and e-readers, transport ticketing, ATMs and self-service terminals, and the hardware and operating systems used to access them. If you sell things online to EU consumers, you're almost certainly in scope via the e-commerce provisions.
- It applies to non-EU companies. The EAA follows the market, not the company's registered address. A Sri Lankan, American, or British business selling into the EU is held to the same requirements as a company in Berlin. This is the GDPR playbook again: EU market access comes with EU rules.
- The microenterprise exemption. Service providers with fewer than 10 employees and annual turnover (or balance sheet total) under €2 million are exempt from the service requirements. It's an AND condition, and it covers services, not products. Growing past either threshold means growing into the obligation — building inaccessibly because you're small today is debt with a known due date.
- Other outs are narrow. There's a "disproportionate burden" defense and a "fundamental alteration" defense, but both require documented assessment — you can't just assert them after a complaint arrives. Transition arrangements exist (e.g. for service contracts concluded before the deadline), but for anything you're building or redesigning now, the requirements simply apply.
- Enforcement varies by member state. The EAA is a directive, so each of the 27 member states transposed it into national law with its own market surveillance authority and its own penalties — fines range roughly from thousands to hundreds of thousands of euros depending on the country, and audits and complaint handling are already underway. Your exposure isn't one regulator; it's every EU market you sell into.
The United States: litigation, not regulation
The US never needed an EAA because plaintiffs' firms built an enforcement industry on top of the ADA. Web accessibility suits have run into the thousands of filings per year for years, and the trend through 2025 was still upward — with e-commerce the large majority of cases and a striking share of defendants being repeat defendants: companies that settled, did superficial remediation, and got sued again by a different plaintiff. Half-fixing accessibility after a demand letter is the most expensive possible way to do this work.
The technical yardstick: EN 301 549 and WCAG
Regulations don't invent their own technical criteria; they point at standards. In Europe, that's EN 301 549, the harmonized standard whose web requirements incorporate WCAG 2.1 Level AA — with an updated version aligning to WCAG 2.2 in the pipeline. In the US, courts and the DOJ have consistently treated WCAG (2.1 or 2.2, Level AA) as the reference point. So the practical engineering target is the same everywhere: WCAG 2.2 AA.
WCAG 2.2: what actually changed for web teams
WCAG 2.2 added nine success criteria on top of 2.1 (and retired 4.1.1 Parsing). The Level A/AA additions — the ones compliance targets care about — are refreshingly practical:
| Criterion | Level | What it means in practice |
|---|---|---|
| 2.4.11 Focus Not Obscured (Minimum) | AA | Sticky headers, cookie banners, and chat bubbles must not fully cover the element that has keyboard focus |
| 2.5.7 Dragging Movements | AA | Anything drag-based (sliders, kanban, reorder lists) needs a non-drag alternative — buttons, click-to-place |
| 2.5.8 Target Size (Minimum) | AA | Pointer targets at least 24×24 CSS pixels, with limited exceptions (inline links, adequate spacing) |
| 3.2.6 Consistent Help | A | Help mechanisms (contact link, chat widget) appear in the same place across pages |
| 3.3.7 Redundant Entry | A | Don't make users re-type information they already gave you in the same flow |
| 3.3.8 Accessible Authentication (Minimum) | AA | No cognitive tests to log in — allow paste, password managers, passkeys; no "transcribe this squiggle" |
(Focus Appearance and the enhanced variants of Focus Not Obscured and Accessible Authentication live at AAA — worth reading, rarely mandated.)
Notice the theme: these aren't screen-reader-specific rules. They're about motor impairments, low vision, and cognitive load — which is to say, about your users on a phone, on a train, with a password manager. Blocking paste on a password field was always hostile design; now it's a WCAG failure.
WCAG 3, briefly. The W3C is developing WCAG 3 with a fundamentally different shape — outcome-based requirements and graded scoring (Bronze/Silver/Gold) instead of binary pass/fail. It's still a Working Draft, years from final, and no regulator asks for it. Track it; don't build to it.
The 80/20 of real accessibility bugs
Across the audits and remediations we've done, a small set of failures accounts for the overwhelming majority of real user impact. Fix only these and you eliminate most barriers on a typical site.
1. Focus management in SPAs (and keyboard traps)
Server-rendered pages got focus management for free: navigate, page loads, focus resets. SPAs broke that contract — route changes that don't move focus, modals that don't trap it, deleted DOM nodes that strand focus at <body> are the signature bugs of client-side routing.
A modal needs three things: focus moves in when it opens, stays in while it's open, and returns to the trigger when it closes. Escape must work. The shape of it in React:
import { useEffect, useRef } from "react";
const FOCUSABLE =
'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])';
export function Modal({ open, onClose, title, children }: ModalProps) {
const dialogRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!open) return;
// Remember what had focus, then move focus into the dialog
triggerRef.current = document.activeElement as HTMLElement;
dialogRef.current?.focus();
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") onClose();
if (e.key !== "Tab") return;
// Wrap focus at the edges instead of letting it escape
const nodes = dialogRef.current!.querySelectorAll<HTMLElement>(FOCUSABLE);
if (nodes.length === 0) return;
const first = nodes[0];
const last = nodes[nodes.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
document.addEventListener("keydown", onKeyDown);
return () => {
document.removeEventListener("keydown", onKeyDown);
triggerRef.current?.focus(); // hand focus back on close
};
}, [open, onClose]);
if (!open) return null;
return (
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabIndex={-1}
>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
);
}
Better yet: the native <dialog> element with showModal() gives you focus trapping, Escape handling, and ::backdrop for free in every modern browser, and libraries like Radix and React Aria have solved this thoroughly. The snippet is the contract — whatever gives you these behaviors is fine; a div with an onClick is not.
The inverse bug — the keyboard trap — is a WCAG Level A failure that shows up constantly in embedded widgets, custom date pickers, and third-party chat bubbles. Test for it the cheap way: put the mouse down and tab through the whole page. If you get stuck anywhere, so does everyone who can't use a pointer.
2. Missing accessible names
An icon button with no name is announced as, literally, "button." These are everywhere — hamburger menus, close buttons, social icons, card links wrapping an image with empty alt. The fix costs one attribute:
<button aria-label="Close navigation">
<svg aria-hidden="true" ...></svg>
</button>
Rules of thumb: visible text beats aria-label (it works for voice-control users who say what they see); decorative SVGs get aria-hidden="true"; informative images get real alt text, and purely decorative ones get alt="" — not a missing attribute, an empty one.
3. Contrast
The most commonly failed criterion in every large-scale scan, and the easiest to prevent. WCAG 2.x requires 4.5:1 for normal text, 3:1 for large text and UI components. The fatal pattern: light-gray-on-white "secondary" text at 2.8:1, approved on a calibrated display in a dark room. Encode contrast into design tokens (more below) and this class of bug stops being possible.
4. Forms without labels or usable errors
Placeholder text is not a label — it vanishes on input and usually fails contrast anyway. Every input needs an associated <label>, and errors must be tied to the field they describe:
<label for="email">Email address</label>
<input id="email" name="email" type="email" autocomplete="email"
aria-describedby="email-error" aria-invalid="true" />
<p id="email-error">Enter an email address, like name@example.com.</p>
aria-describedby gets the error announced with the field; autocomplete satisfies Redundant Entry. On submit failure, move focus to the first invalid field or an error summary — don't just paint things red and hope.
5. Motion without an exit
Autoplaying animation, parallax, and scroll-triggered movement are genuinely harmful to people with vestibular disorders — nausea, not mere annoyance. Every OS exposes a "reduce motion" preference, and CSS reads it directly:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
The blunt version above is a defensible baseline; the craftsman's version handles it per effect — cross-fade instead of slide, static image instead of ambient video. For JavaScript-driven animation, check matchMedia("(prefers-reduced-motion: reduce)") before you start the ticker. Our own site runs canvas particle effects and WebGL backdrops, and every one checks this preference and degrades to a static treatment. Canvas and WebGL deserve special mention: a <canvas> is a black box to assistive technology, so anything meaningful rendered inside it needs a text alternative outside it, and anything decorative should be aria-hidden. The same logic covers no-JS fallbacks — if content only becomes visible via a scroll-reveal observer, force-reveal it in <noscript> styles or users without JavaScript get a blank page.
Semantic HTML first, ARIA as a last resort
The first rule of ARIA, per the W3C itself: don't use ARIA when a native element does the job. A <button> gives you keyboard activation, focus, and the correct role for free. A <div role="button"> gives you the role and nothing else — you now owe tabindex="0", Enter and Space handlers, and disabled-state semantics, and you will get at least one wrong. The anti-patterns we see most in audits:
- Redundant roles (
<button role="button">,<nav role="navigation">) — harmless noise, but a sign ARIA is being cargo-culted. - ARIA as a paint-over —
role="checkbox"on a div with no keyboard behavior. Screen readers announce a checkbox; keyboards can't check it. Worse than no ARIA, because it promises functionality that isn't there. aria-hidden="true"on focusable content — the element vanishes from the accessibility tree but stays in tab order, so keyboard focus lands on silence.- Live-region abuse — announcing everything, or injecting a live region at the same moment as its first message (it must exist in the DOM before content changes, or the announcement is often dropped).
Done right, a live region is small and boring — one persistent, visually-hidden node that you write status messages into:
// Rendered once at app level, present from first paint
function StatusAnnouncer({ message }: { message: string }) {
return (
<div aria-live="polite" role="status" className="visually-hidden">
{message}
</div>
);
}
// Usage: setStatus("3 results found") / setStatus("Item added to cart")
aria-live="polite" waits for the screen reader to finish speaking; reserve assertive for genuinely urgent interruptions. If nothing announces result counts, loading completions, or cart updates, screen reader users are left guessing whether their action did anything at all.
Accessibility in component-driven development
Component architecture is the best thing to happen to accessibility economics: fix the Button, the Input, the Dialog once, and every consumer inherits the fix. It's also the worst thing, because a broken primitive replicates its bug across the entire product. That asymmetry tells you where to spend review effort: on the design system's primitives.
Automated testing belongs in CI; with Playwright and axe-core it's a few lines per page:
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("home page has no detectable a11y violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"])
.analyze();
expect(results.violations).toEqual([]);
});
test("checkout modal is accessible while open", async ({ page }) => {
await page.goto("/pricing");
await page.getByRole("button", { name: "Start a project" }).click();
const results = await new AxeBuilder({ page })
.include('[role="dialog"]')
.analyze();
expect(results.violations).toEqual([]);
});
Run it against interactive states, not just initial page loads — the open modal, the expanded menu, the form in its error state. That's where regressions hide.
But understand what automation can't see. Deque's own large-scale study found automated tools identified 57% of accessibility issues by volume — a figure that's high partly because the machine-detectable failures (contrast, missing alt, missing labels) are also the most frequent ones; measured by WCAG criteria coverage, tooling checks a clear minority, and other analyses put practical detection meaningfully lower. The categories automation cannot judge — is the focus order logical? does the accessible name make sense? can a human actually complete the task? — are exactly the ones that determine whether a real person can use your product. A green axe run means you haven't failed the easy checks. It does not mean you're accessible.
Screen reader testing: the 15-minute habit
Nothing recalibrates a developer faster than hearing their own product. You don't need to be an expert user; you need a smoke-test loop.
VoiceOver (macOS, built in): toggle with Cmd+F5. VO keys are Ctrl+Option; VO+Right Arrow reads the next item, and Ctrl+Option+U opens the rotor — a menu of the page's headings, links, landmarks, and form controls, which is how real users actually navigate. If your rotor shows "link, link, link, button" with no names, or a heading list that skips from h1 to h4, you've found the bug already. Test in Safari; it's the pairing VoiceOver is engineered for.
NVDA (Windows, free): download from nv-access.org, test in Chrome or Firefox. H jumps between headings, Tab between focusable elements, Insert+F7 lists all links and headings.
The drill for any new feature: navigate to it by keyboard only, listen to what's announced at every stop, and complete the core task with your eyes off the pointer. Fifteen minutes. Do it before the PR, not after the complaint.
The overlay trap
There is an entire product category promising to make your site compliant by adding one <script> tag — an "accessibility overlay" that claims to detect and fix issues at runtime. Be very skeptical.
The practitioner consensus is strongly negative: hundreds of accessibility professionals and disabled users have signed the public Overlay Fact Sheet advising against relying on them, and many assistive-technology users report that overlays interfere with the screen readers and customizations they already have. The record backs the skepticism up. In early 2025, the US FTC took action against accessiBe, one of the largest overlay vendors, over claims its product could make any website compliant — a $1 million settlement and an order against unsubstantiated compliance claims. And the lawsuit data is unambiguous: a substantial share of US accessibility suits each year hit sites that have an overlay installed, and some settlements now explicitly require removing the widget and remediating the source code.
The logic is straightforward: an overlay cannot restructure your DOM, fix your focus order, or invent accessible names your markup never had. Your code is what assistive technology reads, and what you'll be judged on. A script tag doesn't transfer that responsibility; at worst, it documents that you knew about the problem and chose not to fix it.
Building it into process
Accessibility fails as a project and works as a property of your process. What we've found actually sticks:
- Put it in the definition of done. "Keyboard operable, axe-clean, announced sensibly" is a per-story acceptance criterion, not a quarterly initiative. The repeat-defendant lawsuit statistics are what "we'll fix it later" looks like at scale.
- Encode contrast in design tokens. Don't audit color pairs; make invalid pairs unrepresentable. Define foreground/background tokens as pairs with verified ratios, in both themes. On our own site every accent theme ships an
--on-accenttoken alongside--accent, because "which text color goes on this background" is a decision the system should make once — including the non-obvious cases where a dark accent needs light text. Validate ratios in CI when tokens change. - Automate the floor, schedule the ceiling. axe-in-Playwright on every PR catches regressions on the machine-checkable portion. A periodic expert audit — ideally including paid testing by disabled users — catches the rest. For EAA purposes, keep the audit trail: documented conformance against EN 301 549 is your evidence.
- Fix the primitives first. One week hardening
Button,Input,Select,Dialog, andToastimproves every screen simultaneously. Remediation that starts anywhere else is bailing with a teaspoon. - Assign ownership. "Everyone's responsibility" means nobody's. One person owns the standard, triages audit findings, and reviews the primitives — even at a small studio, even part-time.
The business case, beyond the subpoena
Compliance gets accessibility into the budget; the returns keep it there. Tens of millions of people in the EU alone live with some form of disability — before you count temporary and situational impairments: the broken wrist, the bright sunlight, the sleeping baby on one arm. That's not an edge case; it's a market segment larger than most countries you'd localize for.
The correlated wins are real, too. Semantic HTML is what search crawlers and, increasingly, AI agents parse — the accessibility tree is machine-readability. Captions get watched on mute. Keyboard support is what power users demand anyway. Target sizes and clear errors lift conversion for everyone. Nearly every accessibility improvement we ship makes the product measurably better for users with no disability at all — the strongest tell that "accessibility" was always just a specific lens on quality.
Takeaways
- The EAA has been enforceable since June 28, 2025. It covers e-commerce, banking, transport, e-books, and more — including non-EU companies selling into the EU. Enforcement and penalties vary by member state.
- The microenterprise exemption (under 10 employees and under €2M turnover) is narrow, services-only, and disappears the moment you grow.
- The practical target everywhere is WCAG 2.2 AA (via EN 301 549 in the EU). WCAG 3 is a draft — track it, don't build to it.
- Most real-world impact comes from a short list: focus management, keyboard traps, accessible names, contrast, form labels and errors, unrespected motion preferences. Fix these first.
- Semantic HTML before ARIA, always. ARIA that promises behavior your code doesn't deliver is worse than none.
- Automate axe in CI, but know its ceiling — roughly half of issues by volume, far less of what determines task success.
- Fifteen minutes of VoiceOver or NVDA per feature, before the PR ships.
- Overlays don't make you compliant, don't stop lawsuits, and drew an FTC enforcement action over marketing claims. Fix the source.
- Make accessibility a property of the system: definition of done, contrast-safe design tokens, hardened primitives, one accountable owner.
- Do it because it's the law if you must — it pays you back as quality, reach, and conversion either way.