Official Sync:2026-04-07

Developer Compliance Pathway

What engineers building EAA-compliant software actually need to know, with the clauses, the patterns, and the tools to verify the work.

Last reviewed: 2026-04-07

Semantic HTML and the accessibility tree

Get the HTML right and most other accessibility work gets a lot easier. Get it wrong and no amount of ARIA will save you.

What the law says

EN 301 549 Clause 9 pulls in WCAG 2.1 Level A and AA wholesale for web content. Two criteria do most of the heavy lifting here: 1.3.1 (Info and Relationships) and 4.1.2 (Name, Role, Value). Between them, they say that whatever a sighted user can see has to be exposed in code, and that every interactive thing has to tell assistive technology what it is, what it's called, and what state it's in. Annex I Section III of the EAA puts this in plainer terms: a service has to be usable through more than one sensory channel. For a screen reader user, that channel is the accessibility tree the browser builds from your DOM. If the tree comes out garbled, the service is non-conformant. Doesn't matter how the page looks.

What it means in practice

Use the native element. Want a button? `<button>`. Want a link? `<a href>`. Yes, `<div onClick>` is faster to type. It also makes you personally responsible for focus, keyboard, role, and state, and in my experience nobody gets all four right on the first attempt. Clickable cards are the canonical version of this mistake. A designer gives you a tile that should navigate when clicked. You wrap the whole thing in a `<div onClick>`. It looks fine in the design review. Then a keyboard user can't reach it, a screen reader walks past it, and your QA pass two weeks later turns into a remediation sprint. The fix is to wrap the tile in `<a href>` instead, or `<button>` if it's an action rather than navigation. The browser does the rest for free. Custom dropdowns, modals, and tab panels are the other graveyard. They look like a 30-minute job. They aren't. Before you write your own, read the W3C ARIA Authoring Practices for the pattern you're about to build. Honestly, just lift one from Radix or Headless UI and move on with your day.

Common pitfalls

  • Clickable cards built as `<div onClick>`. Keyboard users can't reach them, screen readers don't see them, and the focus order goes weird.
  • Headings used for styling instead of structure. If your page jumps h1 → h4 → h2 → h5 because the designer wanted a small caption somewhere, it's unnavigable for anyone using a heading shortcut.
  • `display: none` when you actually wanted to hide something visually but keep it for screen readers. That's a visually-hidden utility class. The other way around (visible to sighted users, gone for screen readers) is `aria-hidden="true"`. Mixing these up is depressingly common.

How to verify it

Open Chrome DevTools, hit the Elements panel, then the Accessibility tab. Click on any interactive thing on the page. If the Name field is empty, the Role says `generic`, or the State doesn't match what you can see, that's a bug to fix. Next: turn off CSS. Firefox lets you do this from the View menu, or you can grab a bookmarklet for Chrome. Look at the unstyled page. If it reads top to bottom in roughly the order you'd want a screen reader to announce it, your markup is doing its job. If it doesn't, the fix is in the HTML, not the CSS. Last one: put your hands on the keyboard and Tab through the page. Every interactive element should get focus, the order should match what you can see, and you should always be able to tell where focus is. Anything reachable by mouse but not by Tab is broken. Anything that receives focus invisibly is broken in a different way.

AccessibilityRef tools that help

Keyboard interaction and focus management

If it works with a mouse, it has to work with a keyboard. No exceptions, no 'we'll fix it later'.

What the law says

Four WCAG criteria do most of the work here. SC 2.1.1 (Keyboard) says everything has to be operable with a keyboard, no funky keystroke timing required. SC 2.1.2 (No Keyboard Trap) says focus has to be able to leave any element it lands on. SC 2.4.3 (Focus Order) says the order needs to make sense. SC 2.4.7 (Focus Visible) says you have to be able to see where focus is at all times. For non-web software and mobile apps, EN 301 549 Chapter 11 says the same thing in slightly different words. If you're shipping under EAA, all four are mandatory.

What it means in practice

Most keyboard bugs come down to three things: something interactive that Tab won't reach, focus that jumps around in the wrong order, or focus that's invisible when it lands. The usual culprits are equally boring. `tabindex="-1"` slapped onto a button by someone who didn't understand what it does. A modal that opens without moving focus into it. A reset stylesheet with `outline: none` and no replacement. Honestly, go grep your codebase for `outline: none` right now. I'll wait. If anything comes up, that's where you start. For anything that opens a new layer on top of the page — modals, drawers, side panels — focus has to move into it when it opens, has to stay trapped inside while it's open, and has to land back on the button that opened it when it closes. There's a `focus-trap` library on npm that handles this correctly. Just use it. Writing your own is a bad use of your afternoon unless you've actually read the WAI-ARIA dialog pattern from top to bottom. Custom widgets are harder. Comboboxes, tab panels, tree views, sliders — the keyboard model gets fiddly. Arrow keys, Home, End, Escape, Page Up, Page Down, all with specific expected behaviour depending on the widget. The Authoring Practices Guide spells it all out. Building one from scratch is a day's work, easily. Anyone who tells you otherwise is going to ship a half-baked widget.

Common pitfalls

  • `outline: none` in a CSS reset with nothing put back. Every keyboard user on your site is now flying blind.
  • Modals that don't trap focus. Users tab out of the modal into the page behind it and have no idea the modal is even still open.
  • Custom dropdowns where the arrow keys do nothing. Mouse users get the full menu, keyboard users get a button that opens a list they can't move through.

How to verify it

Unplug the mouse. Tape over the trackpad. Whatever it takes. Then try to actually use your product. If you can't get through the core flows — sign-up, search, checkout, whichever ones matter for you — you've got work to do. While you're tabbing around, watch where the focus indicator lands. The order should match what you see on screen. When it jumps around, it usually means your DOM order doesn't match the visual order — flex `order`, absolute positioning, or some grid trickery is to blame nine times out of ten. For modals: open one and press Tab a bunch of times. Focus should cycle inside the modal forever. Then hit Escape. The modal should close and focus should land back on whatever button opened it. If either of those things doesn't happen, the modal is broken.

AccessibilityRef tools that help

ARIA: when to use it, when not to

ARIA exists to patch what HTML can't express on its own. Bad ARIA is genuinely worse than no ARIA at all, and there's a lot of bad ARIA out there.

What the law says

WCAG 4.1.2 (Name, Role, Value) is the criterion that matters. It says every UI component has to expose its name and role programmatically, plus its current state and any properties, plus any changes to those things — all in a way assistive technology can pick up. Notice what it doesn't say. It doesn't say 'use ARIA'. It says the information has to be exposed. If a native HTML element can carry it, that's the right answer. ARIA is for when HTML genuinely can't describe what you're building. EN 301 549 9.4.1.2 (web) and 11.4.1.2 (software) carry the same requirement straight through.

What it means in practice

The W3C literally calls it the 'first rule of ARIA use': don't use ARIA. They mean it. If `<button>`, `<a>`, `<input>`, `<select>` or `<dialog>` can do the job, use one of those. Native elements bring focusability, keyboard support, role exposure and state for free. ARIA gives you the role label and absolutely nothing else — everything beyond that is on you. When you actually need it: live regions for stuff that updates without a page navigation (`aria-live="polite"` for routine updates, `aria-live="assertive"` only when interrupting the user is genuinely justified). `aria-label` for icon-only buttons that don't carry visible text. `role="region"` with `aria-labelledby` for sections that need a screen-reader landmark and don't have a native equivalent. The trouble starts with ARIA that's redundant, wrong, or actively contradictory. `<button role="button">` is harmless, just pointless. `<a role="button">` is genuinely broken — screen readers announce it as a button but the keyboard still treats it as a link, so Enter works and Space doesn't. The worst version is `<div role="button" tabindex="0">` with no keydown handler. The screen reader says 'button', the user presses Enter, nothing happens, and you've shipped a paperweight. For things like toasts, form errors, search results updating in place, you need `role="status"`, `role="alert"`, or a wrapping `aria-live`. Without one, the change happens visually and screen reader users have no idea anything happened at all.

Common pitfalls

  • Reaching for `role` when a native element exists. `<div role="button">` is almost always wrong, `<button>` almost always right.
  • Slapping `aria-live="assertive"` on everything because it feels safer. It interrupts the user mid-sentence. Use `polite` unless the thing genuinely can't wait.
  • Putting `aria-label` on elements that already have visible text. Six months later the visible label gets updated and the aria-label doesn't, and now the screen reader is announcing something that doesn't exist on screen.

How to verify it

Fire up a real screen reader. NVDA is free on Windows. VoiceOver ships with macOS. Tab through your page and listen. The role should match what you can see, the name should make sense out loud, and the state (pressed, expanded, checked) should match the visual state. For live regions, trigger the update and listen for it. If nothing gets announced, your live region isn't doing its job. The two usual reasons: you added the live region to the DOM at the same time as the content (it has to exist first), or the new content is buried in a child element where the live region doesn't apply. The ARIA Assistant tool on this site will parse your HTML and flag the obvious WAI-ARIA misuses. Worth running as a first pass before you start the manual screen reader work.

AccessibilityRef tools that help

Forms, errors, and validation

Forms are where the legally interesting stuff happens: sign-up, checkout, applications. They're also where the bugs love to hide.

What the law says

Six WCAG criteria do most of the work for forms. 1.3.1 (Info and Relationships) for properly associated labels. 3.3.1 (Error Identification) for actually naming what went wrong. 3.3.2 (Labels or Instructions) for telling users what you expect from them up front. 3.3.3 (Error Suggestion) for telling them how to fix it. 3.3.4 (Error Prevention for Legal/Financial/Data) for letting users review and undo before they commit something they can't take back. And 4.1.2 (Name, Role, Value) for making sure assistive technology can see the field type and state. WCAG 2.2 added two more that every team should know about: 3.3.7 (Redundant Entry), which says you can't make users retype info they already gave you, and 3.3.8 (Accessible Authentication), which is what kills the 'enter the third character of your password' nonsense. EN 301 549 v3.2.1 makes both of those mandatory.

What it means in practice

Every form input needs a real `<label>` element with a matching `for` attribute. Placeholder text is not a label. Floating labels that disappear into thin air on focus are not labels either, unless there's an actual `<label>` underneath them. When someone submits a form with errors, four things need to happen. Stop the submission. Move focus either to the first broken field or to a summary at the top of the form with anchor links to each problem. Tie each error message to its field with `aria-describedby`. And make sure a screen reader user knows there are errors — either by the focus move itself or via a live region. Inline validation needs care. Don't fire an error the second a user starts typing into an empty field — that's how you get a screen reader yelling 'invalid' on every keystroke until the user gives up and closes the tab. Validate on blur, or after a sensible debounce. Authentication is the bit most teams break. Stop disabling paste on password fields. Stop disabling paste on OTP fields. Stop disabling paste on 'confirm your email' fields. All three break password managers and all three are now WCAG 2.2 fails. Stop making people retype their email address (3.3.7). And if you can, ship passkeys or magic links — both are explicitly listed as compliant alternatives in 3.3.8.

Common pitfalls

  • Placeholder text doing the job of a label. The moment the user starts typing, the label vanishes and they're guessing what the field was for.
  • Paste disabled on password, OTP or 'confirm email' fields. Breaks every password manager on the market and now fails WCAG 2.2 directly.
  • A vague 'There was an error' banner at the top of the form with no indication of which field is broken. Now users have to hunt for the red border.

How to verify it

Submit the form with deliberately bad data. Error messages should land near the fields they belong to, not just as a generic banner at the top. Either the first broken field should get focus, or a summary should get focus with links into each problem. With a screen reader running, you should hear how many errors there are and what they are. Now submit the form successfully, with the screen reader still on. You should hear a confirmation. If the only success indicator is a green tick on screen and no live region announcement, screen reader users have no way of knowing the form actually went through. Last test: run a password manager at the form. If 1Password, Bitwarden or Apple Keychain can fill the right fields, your autocomplete attributes and input types are correct. If they can't, fix them. Broken autocomplete is its own WCAG 2.2 failure.

AccessibilityRef tools that help

Mobile and platform accessibility APIs

Mobile apps live under EN 301 549 Chapter 11, not Chapter 9. The principles rhyme with web work but the actual code is different.

What the law says

Chapter 11 of EN 301 549 covers software, including native mobile apps. The clause that does the heavy lifting is 11.5.2 — it says any software with a user interface has to expose enough information to assistive technology that the AT can rebuild that interface, and it has to use the documented platform accessibility services to do it. In practice that means UIKit accessibility APIs (`accessibilityLabel`, `accessibilityTraits`, `accessibilityValue`) or the SwiftUI accessibility modifiers on iOS. On Android, the View accessibility API (`contentDescription`, `accessibilityNodeInfo`) or `Modifier.semantics` if you're using Compose. Both platforms also require working support for the system screen reader (VoiceOver, TalkBack), the system text scaling (Dynamic Type, font scale), and the system high-contrast modes.

What it means in practice

Same advice as the web: lean on the native components. SwiftUI's `Button`, UIKit's `UIButton`, Android's `Button`, Compose's `Button` — all of them expose the right traits to the screen reader without you doing anything. The moment you build a custom tappable view, you've taken on the job of setting the role and label yourself, and that's where bugs creep in. On iOS, every interactive element needs `accessibilityLabel`, the right `accessibilityTrait` (`.button`, `.link`, `.header`, etc.), and an `accessibilityValue` where it makes sense. Group decorative bits with `accessibilityElements` so VoiceOver doesn't read every meaningless background element one at a time. Test with the Accessibility Inspector in Xcode, then test again with VoiceOver running on a physical device. On Android, the equivalent is `contentDescription` (or just a `text` value if the element already has visible text). Decorative images get `importantForAccessibility="no"` so TalkBack skips them. Test with TalkBack on a real device, plus the Accessibility Scanner app from the Play Store. Dynamic Type and font scaling aren't optional. If a user cranks their system text size to the largest setting and your layout falls apart, that's a Clause 11.7 fail straight away. The fix is to stop using fixed point sizes — use semantic font styles like `UIFont.preferredFont(forTextStyle:)` on iOS or `sp` units on Android — and then actually test every screen at the largest setting before you ship. Touch targets need to clear WCAG 2.2 SC 2.5.5 (24×24 CSS pixels minimum), and on mobile you should be looking at the platform guidelines on top of that — 44×44 points on iOS, 48×48 dp on Android. Treat the web minimum as the absolute floor and the platform numbers as the real target.

Common pitfalls

  • Custom tappable views shipped without an `accessibilityLabel` or `contentDescription`. The screen reader either says nothing or announces a generic 'button' that gives the user no idea what it does.
  • Hard-coded font sizes that ignore the user's system text setting. Anyone with low vision who needs the text larger is stuck, and the app stops being usable for them.
  • Tap targets under 44×44 points on iOS or 48×48 dp on Android. Users with any kind of motor impairment can't hit them reliably.

How to verify it

Turn on VoiceOver (iOS Settings → Accessibility → VoiceOver) or TalkBack (Android Settings → Accessibility → TalkBack) and try to actually use your own app. Every interactive thing should be reachable, every action should announce something that makes sense, and you should be able to get through the main flows without giving up. Then turn the screen reader off and push the system text size to the largest setting. iOS: Settings → Display & Brightness → Text Size, then turn on Larger Accessibility Sizes under Accessibility. Android: Settings → Display → Font size. Walk through every screen. Anything that gets clipped, overflows the edge, or becomes unreadable is a fail you need to fix. Finally, run Accessibility Inspector in Xcode or Accessibility Scanner on Android against each screen. They won't catch everything, but they'll find the obvious stuff — missing labels, contrast misses, tiny touch targets — and hand you a list to work through.

AccessibilityRef tools that help

Important Legal Disclaimer

This tool is a self-assessment aid only and does not constitute legal advice or a formally certified compliance assessment. Outputs — including reports, scores, checklists, and accessibility statements — are for internal use and should be reviewed by a qualified legal representative or independent accessibility auditor before being relied upon for regulatory, procurement, or public-disclosure purposes. All assessment risk lies with the internal assessor. accessibilityref, its developers, and staff accept zero liability for losses arising from use of or reliance on these outputs. Always verify against official sources: the W3C WCAG 2.2 Recommendation, the European Accessibility Act (Directive 2019/882), and your national enforcement authority.