Diego Betto
Photo by Compagnons on Unsplash

Diego Betto · October 13, 2026 · 14 min di lettura

European Accessibility Act and WCAG 2.2: a practical compliance guide for developers

What the European Accessibility Act requires, who must comply, penalties, and how to make a site WCAG 2.2 compliant: keyboard, contrast, screen readers, ARIA and testing.

Condividi:XLinkedInFacebookWhatsApp

Since 28 June 2025, digital accessibility is no longer a nice-to-have “if there’s time left” for a large share of online services sold to consumers in the European Union: it’s a legal requirement, with a supervising authority and penalties. That’s the effect of the European Accessibility Act (EAA, Directive (EU) 2019/882), transposed into national law by every member state — in Italy, for example, by Legislative Decree 82/2022.

A year and a few months later, plenty of sites are still at “we installed a widget”. This article does two things: it explains concretely what the law requires and of whom, and then — the longer part — translates it into code: HTML, CSS, ARIA and automated tests.

ℹ️ Nota

I’m not a lawyer and this isn’t legal advice. To find out whether your company is in scope, and with which exceptions, the sources are the directive, your country’s transposition and the guidance from your national authority. Here I focus on the technical side.

Who has to comply

The EAA covers a specific list of products and services provided to consumers. For web developers, the part that matters is the services:

Service Examples of what must be accessible
E-commerce Any B2C online shop: catalog, cart, checkout, account
Consumer banking services Online banking, apps, account opening, payments
Passenger transport (air, rail, bus, waterborne) Booking sites and apps, e-tickets, real-time travel info
Electronic communications Customer portals of telecom and internet providers
Access to audiovisual media services Streaming platforms, program guides
E-books Stores, readers, dedicated software

Three things to know straight away:

  • B2B is out. The law is about services to consumers. Software sold only to businesses isn’t covered (but if it has an area used by end consumers, think it through).
  • Microenterprises are exempt, but only for services. Fewer than 10 employees and annual turnover or balance sheet total not exceeding €2 million: no obligation for services. The exemption doesn’t apply to products.
  • Transition period. Service contracts agreed before 28 June 2025 can continue until they expire, for at most five years; products already used to deliver the service before that date can stay in use until 28 June 2030. It’s not a general postponement: a site or app launched or rebuilt after June 2025 must comply.

There’s also the disproportionate burden exception: if compliance would cost too much relative to the company’s size and resources, it can be invoked, but it must be backed by a written assessment that is kept on file and renewed periodically. It’s not a free “no”.

Who enforces it and what’s at stake

Each member state designates market surveillance authorities and sets its own penalties, which the directive requires to be “effective, proportionate and dissuasive”. The amounts vary a lot from country to country. To give an idea, in Italy the authority for services is AgID, which published operational guidelines in March 2026 and handles user complaints, and the penalties are:

Violation (Italy, Legislative Decree 82/2022) Administrative fine
Failure to meet accessibility requirements €5,000 to €40,000
Failure to comply with the authority’s corrective measures €2,500 to €30,000
Failure to cooperate with the authority €2,500 to €30,000

On top of the fines there’s the less visible but more concrete risk: complaints from users and advocacy groups, and customers who simply can’t buy.

Which technical standard: WCAG 2.1 or 2.2?

The EAA doesn’t reference WCAG directly. The technical reference is the European harmonised standard EN 301 549, and the timeline here is shifting:

  • EN 301 549 v3.2.1 (the harmonised version in force today) points to WCAG 2.1 level AA.
  • EN 301 549 v4.1.1, published by ETSI on 2 September 2026, aligns with WCAG 2.2. It becomes the reference version once cited in the Official Journal of the EU, expected by the end of the year.

The practical advice is simple: target WCAG 2.2 AA now. It’s a superset of 2.1 (apart from one removed criterion), so if you conform to 2.2 you also conform to 2.1, and you won’t have to redo the work in a few months.

What’s new in 2.2, at levels A and AA:

Criterion Level In practice
2.4.11 Focus Not Obscured (Minimum) AA The focused element must not end up under a sticky header or cookie banner
2.5.7 Dragging Movements AA Every drag & drop needs a single-pointer alternative
2.5.8 Target Size (Minimum) AA Click targets at least 24×24 px (or spaced far enough apart)
3.2.6 Consistent Help A Contact and help always in the same place across pages
3.3.7 Redundant Entry A Don’t ask for the same data twice in the same process
3.3.8 Accessible Authentication (Minimum) AA Login without cognitive tests: paste and password managers allowed

Criterion 4.1.1 Parsing was removed: modern browsers handle malformed HTML consistently, so it no longer made sense.

From the law to the code

WCAG is organized around four principles (perceivable, operable, understandable, robust), but in day-to-day work it’s easier to think in areas. These are the ones where I find the most problems in audits.

1. Semantic HTML first

Most accessibility comes from using the right element. A <button> is focusable, activates with Enter and Space, has the “button” role and an accessible name. A <div onclick> has none of that, and rebuilding it by hand takes role, tabindex, keydown handling and ARIA states.

<header>
  <a class="skip-link" href="#main">Skip to content</a>
  <nav aria-label="Main">...</nav>
</header>

<main id="main">
  <h1>Trail running shoes</h1>
  <section aria-labelledby="filters-title">
    <h2 id="filters-title">Filters</h2>
    ...
  </section>
</main>

<footer>...</footer>

Landmarks (header, nav, main, footer), a single h1, a heading hierarchy without gaps, and a skip link as the first focusable element: screen reader and keyboard users jump straight to the content instead of tabbing through the menu on every page.

2. ARIA: less is more

The first rule of ARIA is not to use ARIA if a native element does the same thing. ARIA only changes what gets announced, not behavior: role="button" on a div doesn’t make it keyboard-operable.

Where it’s genuinely needed:

  • aria-label / aria-labelledby to name controls with no visible text (an icon-only button).
  • aria-expanded and aria-controls on menus and accordions.
  • aria-current="page" on the active navigation item.
  • aria-live to announce state changes that don’t move focus.
  • aria-hidden="true" on decorative icons.
<button type="button" aria-expanded="false" aria-controls="mobile-menu">
  <svg aria-hidden="true">...</svg>
  <span class="visually-hidden">Menu</span>
</button>

3. Keyboard navigation and focus

Try your site without a mouse: Tab, Shift+Tab, Enter, Space, Esc, arrow keys. If there’s a point where you don’t know where you are or can’t move on, that’s a level A problem.

/* Never remove the outline without replacing it */
:focus-visible {
  outline: 3px solid var(--focus-color);
  outline-offset: 2px;
}

/* 2.4.11: focus must not end up under the sticky header */
html {
  scroll-padding-top: 5rem; /* header height + margin */
}

For modals, use the <dialog> element with showModal(): it natively handles the focus trap, closing with Esc and making the rest of the page inert. It used to be the hardest component to get right by hand; now it’s free.

function ConfirmDialog({ open, onClose, onConfirm }: { open: boolean; onClose: () => void; onConfirm: () => void }) {
  const ref = useRef<HTMLDialogElement>(null);

  useEffect(() => {
    if (open) ref.current?.showModal();
    else ref.current?.close();
  }, [open]);

  return (
    <dialog ref={ref} onClose={onClose} aria-labelledby="dialog-title">
      <h2 id="dialog-title">Delete this address?</h2>
      <button onClick={onClose}>Cancel</button>
      <button onClick={onConfirm}>Delete</button>
    </dialog>
  );
}

Avoid positive tabindex values (tabindex="3"): they scramble the natural order. Use only 0 (make focusable) and -1 (focusable from script, not with Tab).

4. Contrast and color

The minimum ratios for level AA:

  • 4.5:1 for normal text.
  • 3:1 for large text (from 24 px, or 18.66 px bold).
  • 3:1 for UI components and meaningful graphics: input borders, icons, the focus indicator.

Color must never be the only way to convey information: an invalid field has a red border and a text message; a chart has a legend or patterns, not just different hues.

The most effective way to get this right is to solve it once, in the design tokens: if the design system palette is verified, every component starts out compliant. Don’t forget dark mode: it’s a second set of pairs to check.

5. Forms and errors

Forms are where users with disabilities drop off most, and where the business value is concentrated (checkout, sign-up, login).

<label for="email">Email</label>
<input
  id="email"
  name="email"
  type="email"
  autocomplete="email"
  required
  aria-invalid="true"
  aria-describedby="email-error"
/>
<p id="email-error" class="field-error">Enter a valid email address, e.g. [email protected]</p>
  • Every field has a visible, linked <label>.
  • Correct autocomplete: it helps everyone, and criterion 1.3.5 requires it.
  • The error is textual, specific (it says how to fix it) and linked with aria-describedby.
  • On a failed submit, move focus to the first invalid field, or to an error summary.
  • 3.3.7: if the shipping address has already been entered, offer “same as billing” instead of making the user type it again.

6. Accessible login (3.3.8)

Criterion 3.3.8 forbids relying on cognitive tests for authentication without alternatives. Concretely:

  • Don’t block paste in password fields (onpaste={e => e.preventDefault()} is a violation).
  • Use autocomplete="current-password" / "new-password" so password managers work.
  • “Find the traffic lights” CAPTCHAs or puzzles need an alternative.
  • Passkeys are the ideal solution: no password to remember or type.

7. Targets, motion and responsive

  • 2.5.8: buttons and links at least 24×24 px. 16 px icons in tables and “×” close links are the classic offenders.
  • 2.5.7: reordering with drag & drop? Add “move up/down” buttons.
  • 1.4.10 Reflow: at 320 px wide (a desktop at 400% zoom), no horizontal scrolling.
  • Animations: respect prefers-reduced-motion for animations and parallax.
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

8. Dynamic content and SPAs

In a single-page app, a route change isn’t announced to screen readers the way a page load is. After navigating, update document.title and move focus to the new page’s h1 (with tabindex="-1"). For notifications, add-to-cart confirmations and search results, use a live region:

<div aria-live="polite" class="visually-hidden" id="status"></div>
document.getElementById("status")!.textContent = "Product added to cart (3 items)";

The region must exist in the DOM before the text changes, otherwise many screen readers won’t announce it.

Testing: automated, manual, real

Automated tests only catch part of the problems, typically estimated at between a third and a half. They spot insufficient contrast or an image with no alt; they can’t tell whether the alt actually describes the image or whether the focus order makes sense. But they’re cheap and prevent regressions, so they belong in CI.

With Playwright and axe-core:

// tests/a11y.spec.ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

const pages = ["/", "/products", "/cart", "/checkout"];

for (const path of pages) {
  test(`${path} has no WCAG A/AA violations`, async ({ page }) => {
    await page.goto(path);
    const results = await new AxeBuilder({ page })
      .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"])
      .analyze();

    expect(results.violations).toEqual([]);
  });
}

test("the skip link is the first focusable element", async ({ page }) => {
  await page.goto("/");
  await page.keyboard.press("Tab");
  await expect(page.getByRole("link", { name: "Skip to content" })).toBeFocused();
});

Note the use of getByRole: if your tests find elements by role and accessible name, every end-to-end test becomes a small accessibility test too. The same goes for component tests with Testing Library, which I used in the Vitest guide.

Then the manual checks, at least on critical flows:

  1. Keyboard only across the whole purchase or sign-up path.
  2. Screen readers: NVDA on Windows, VoiceOver on macOS and iOS, TalkBack on Android.
  3. 200% zoom and 320 px width.
  4. Contrast on hover, focus, disabled and dark-theme states.

Finally, whenever possible, test with real users with disabilities: in half an hour they find things no checklist anticipates.

⛔ Pericolo

“Automatic accessibility” widgets (overlays) don’t make a site compliant. They don’t fix the underlying code, they often interfere with the screen readers users already rely on, and in 2025 the US FTC fined one of the best-known vendors $1 million for promising WCAG compliance the product didn’t deliver.

A process, not a project

The most common mistake is treating accessibility as a one-off audit. The site changes every week, and every release can introduce new barriers. What works:

  1. Initial audit on templates and key flows (home, listing, product page, checkout, login, account area), not on every page.
  2. Remediation backlog ordered by impact: first what blocks a task (I can’t pay), then what makes it hard.
  3. Compliant design system: fix the component once, a hundred pages get fixed.
  4. axe tests in CI that fail the build on regressions.
  5. Published accessibility information: the EAA requires you to explain how the service meets the requirements, and to offer a channel for reporting problems.
  6. Training: designers, developers and content editors. An empty alt on an informative image is a content problem, not a code problem.

Quick checklist

  • Landmarks, a single h1, correct heading hierarchy
  • Skip link as the first focusable element
  • Everything reachable and operable by keyboard, no traps
  • Visible focus (:focus-visible), never hidden by sticky headers
  • Contrast 4.5:1 for text, 3:1 for components and large text, dark mode included
  • Informative images with meaningful alt, decorative ones with alt=""
  • Every field with <label>, autocomplete and linked errors
  • Login: paste allowed, password managers working, CAPTCHA alternatives
  • Targets at least 24×24 px, alternatives to dragging
  • No horizontal scroll at 320 px, prefers-reduced-motion respected
  • Modals with <dialog>, route changes and notifications announced
  • Captions for video, transcripts for audio
  • axe tests in CI and manual screen reader checks on critical flows
  • Accessibility information and reporting channel published

Frequently asked questions

❓ Does my brochure website have to comply with the EAA?

A purely informational site that doesn’t sell or deliver one of the services listed in the directive is generally out of scope. If it has an online shop, or lets people book and pay for a covered service, it is in scope. Either way, an accessible site reaches more people, works better with AI agents and ranks better: you don’t need an obligation to do it.

❓ Should I follow WCAG 2.1 or 2.2?

Today the harmonised standard in force points to 2.1 AA, but the new EN 301 549 v4.1.1 aligns with 2.2. Targeting 2.2 AA now costs little extra and saves you from redoing the work once the new version is cited in the Official Journal.

❓ Does a 100 Lighthouse Accessibility score mean compliance?

No. Lighthouse runs a subset of the axe-core rules and only checks what can be verified automatically. A 100 means “no machine-detectable errors”, not “usable by a person with a screen reader”.

❓ Does accessibility hurt performance or SEO?

The opposite. Semantic HTML, text instead of images, a clear heading structure and images with alt are the same things that help search engines and AI agents. And a lightweight site that passes Core Web Vitals is also more usable for people browsing with assistive technology on low-end devices.

Conclusion

The European Accessibility Act turned accessibility from a nice-to-have into a requirement, but the technical side hasn’t changed: it’s still semantic HTML, keyboard support, contrast, well-built forms and testing. If you start from shared components, put axe in CI and manually check the flows that bring in money, you’re already on the right side of the curve — and the site gets better for everyone, not just people using assistive technology.

References:

Condividi:XLinkedInFacebookWhatsApp
Diego Betto

Written by

Diego Betto

Co-Founder & CTO at PAPION. Senior full-stack engineer specializing in React, TypeScript, Node.js, and application security.