EAA Web Accessibility Practical Guide for Frontend Developers | WCAG 2.2 + Automated Testing
On June 28, 2025, a significant piece of legislation came into effect in the EU. The European Accessibility Act (EAA) — a law mandating that people with disabilities have equal access to digital services — applies to virtually all digital services including websites, mobile apps, and internet banking. Fines reach up to €1,260,000 depending on the country, and in November 2025, France's DGCCRF actually filed lawsuits against Auchan, Carrefour, E.Leclerc, and Picard (source: Pivotal Accessibility). If you thought "this doesn't apply to us since we're not an EU service" — the moment you provide services to EU member states, Korean companies fall under its scope as well.
Honestly, my first reaction was "another compliance thing about accessibility," but once I actually applied it to code, I noticed an overall improvement in UX quality. After reading this article, you'll be able to apply the core WCAG 2.1/2.2 criteria required by the EAA in code, and integrate automated testing into your team's workflow. Code examples are based on HTML, with JSX versions included for those using React.
Core Concepts
The Technical Standard EAA Requires: EN 301 549 and WCAG
The EAA uses the EN 301 549 European standard as its technical compliance reference, and the heart of that standard is WCAG 2.1 Level AA. WCAG is organized around four principles (POUR).
| Principle | Meaning | Key Requirements |
|---|---|---|
| Perceivable | All content can be perceived through the senses | Image alt text, color contrast |
| Operable | All functionality is usable with keyboard alone | Keyboard navigation, focus visibility |
| Understandable | Language and error messages are clear | Form labels, error guidance |
| Robust | Compatible with a wide range of assistive technologies | Semantic HTML, ARIA attributes |
This table might feel too abstract at first, but as you work through the code examples that follow, you'll naturally understand which code patterns map to each principle.
What's New in WCAG 2.2
WCAG 2.2, which became an official W3C standard in October 2023, added 9 success criteria to the existing 2.1. The EU plans to update EN 301 549 based on 2.2 in the future, so preparing now will help you avoid rework later.
| Added Item | Requirement |
|---|---|
| Focus Appearance | Focus ring must be at least 2px, with at least 3:1 contrast against the background |
| Target Size | Touch/click targets minimum 24×24 CSS pixels (AA level) |
| Accessible Authentication | Provide an authentication method that doesn't require cognitive puzzles when logging in |
One thing to watch with Target Size: the AA requirement is 24×24px, but AAA recommends 44×44px, and practical guidance often recommends 44px or more. Thinking "24px is enough" can cause friction in real mobile UX, so it's worth being generous.
Accessible Authentication also doesn't simply mean "remove CAPTCHA." It means providing at least one low-cognitive-load alternative such as allowing copy-paste, WebAuthn (passkeys), magic links, or SMS verification codes.
Terminology —
WAI-ARIA(Web Accessibility Initiative – Accessible Rich Internet Applications): A W3C specification that adds roles, states, and properties to HTML so that assistive technologies such as screen readers can understand dynamic UI. Attributes likerole,aria-label, andaria-describedbybelong to this specification.
Practical Application
Example 1: Semantic HTML and Keyboard Navigation
This is a situation you'll encounter frequently in practice — attaching click events to div elements is more common than you'd think. Screen readers can't recognize such elements as buttons, and they can't receive keyboard focus either.
<!-- Bad: inaccessible to both keyboard and screen readers -->
<div onclick="submitForm()" style="cursor:pointer">Submit</div>
<!-- Good: use a semantic button -->
<button type="submit">Submit</button>
<!-- Use aria-label when text is unclear, e.g. icon-only buttons -->
<button type="button" aria-label="Open search">
<svg aria-hidden="true">...</svg>
</button>| Point | Description |
|---|---|
Use <button> |
Built-in keyboard focus and Enter/Space behavior by default |
aria-label |
Use when button text is unclear (e.g. icon buttons); unnecessary when text is present |
type="submit" |
Clearly communicates intent within a form |
You'll sometimes see aria-label used identically to the button text — this causes screen readers to read the same text twice, which sounds awkward. Use aria-label as a supplement only when visible text alone can't convey the meaning.
Example 2: Form Labels and Error Guidance
Using only placeholder and omitting label is also common — the moment focus enters the field, the placeholder disappears and users forget what they're supposed to type. Form accessibility was the area I got the most feedback on when I first took over code reviews for a team.
Basic HTML example:
<!-- Bad: no label -->
<input type="email" placeholder="Enter email" />
<!-- Good: connected label + hint + error message -->
<label for="email">Email address</label>
<input
id="email"
type="email"
aria-describedby="email-hint email-error"
aria-required="true"
aria-invalid="true"
/>
<span id="email-hint">e.g. user@example.com</span>
<span id="email-error" role="alert">
Please enter a valid email address.
</span>React JSX version:
const [hasError, setHasError] = useState(false);
return (
<>
<label htmlFor="email">Email address</label>
<input
id="email"
type="email"
aria-describedby="email-hint email-error"
aria-required="true"
aria-invalid={hasError}
onChange={(e) => setHasError(!e.target.value.includes('@'))}
/>
<span id="email-hint">e.g. user@example.com</span>
{hasError && (
<span id="email-error" role="alert">
Please enter a valid email address.
</span>
)}
</>
);| Attribute | Role |
|---|---|
for / htmlFor + id connection |
Clicking the label focuses the input; connects to screen readers |
aria-describedby |
Connects hint and error messages to assistive technologies |
aria-invalid |
Communicates error state to screen readers |
role="alert" |
Immediately reads dynamically added error messages aloud |
Example 3: Focus Visibility (WCAG 2.2 Focus Appearance)
Many teams globally apply outline: none due to design requirements — I did the same early on and got burned badly during keyboard user testing. WCAG 2.2 specifies both the size and contrast ratio of focus rings.
/* ❌ Pattern to avoid at all costs */
* { outline: none; }
/* ✅ WCAG 2.2 standard: at least 2px, at least 3:1 contrast against background */
/* #005fcc has approximately 5.9:1 contrast ratio on white background — passes AA */
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
border-radius: 2px;
}
/* Focus ring can be hidden on mouse click */
:focus:not(:focus-visible) {
outline: none;
}Tip —
:focus-visibleapplies styles only to keyboard focus, not mouse clicks. It's a modern approach that lets you maintain both design aesthetics and accessibility without globally removingoutline: none.
Example 4: Image Alt Text and Skip Navigation
<!-- Informational image: descriptive alt is required -->
<img src="chart.png" alt="Bar chart showing 35% year-over-year revenue increase in 2025" />
<!-- Decorative image: use alt="" so screen readers ignore it -->
<!-- alt="" alone is sufficient for decorative images; role="presentation" is optional -->
<img src="divider.png" alt="" role="presentation" />
<!-- Skip Navigation: place at the very top of the page -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content">...</main>.skip-link {
position: absolute;
transform: translateY(-100%);
transition: transform 0.2s;
}
.skip-link:focus {
transform: translateY(0);
}Skip Navigation is a small courtesy that prevents keyboard users from having to tab through repetitive menus on every page. It takes about 10 minutes to implement, but the impact is significant.
The difference between alt="" and role="presentation" can be confusing — alt="" alone causes screen readers to ignore the image. role="presentation" removes the element's semantic role entirely; for decorative images, alt="" is sufficient.
Example 5: Color Contrast Requirements
Color contrast was a shock when I properly checked it for the first time. I didn't realize until then that the commonly used #777777 gray text on a white background doesn't meet the 4.5:1 threshold. Around #595959 passes the AA standard.
| Text Type | Minimum Contrast Ratio (WCAG AA) |
|---|---|
| Normal text (below 18pt) | 4.5:1 |
| Large text (18pt or above / Bold 14pt or above) | 3:1 |
| UI components and graphical boundaries | 3:1 |
Validating contrast ratios at the design token level upfront significantly reduces the effort needed for color-related revisions later. Here's an example managed with CSS custom properties.
:root {
/* Contrast ratios noted against white background (#fff) */
--color-text-primary: #1a1a1a; /* contrast ~16.7:1 */
--color-text-secondary: #595959; /* contrast ~7.0:1 */
--color-text-muted: #767676; /* contrast ~4.5:1 — AA boundary */
--color-text-disabled: #a3a3a3; /* contrast ~2.3:1 — disabled only */
}
body {
color: var(--color-text-primary);
}
.hint-text {
color: var(--color-text-secondary);
}You can check contrast ratios immediately using the Colour Contrast Analyser.
Setting Up Automated Testing
This is what I promised in the title — without automation, accessibility management isn't sustainable. You can't manually check every component every time.
jest-axe: Component-Level Automation
pnpm add -D axe-core jest-axe @types/jest-axe// EmailInput.test.tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { EmailInput } from './EmailInput';
expect.extend(toHaveNoViolations);
it('should have no accessibility violations', async () => {
const { container } = render(<EmailInput />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});Tying this to CI automatically catches accessibility regressions with every PR. It's not perfect (automated tools detect only about 40–57% of all issues), but it prevents obvious mistakes from being deployed.
axe DevTools + Lighthouse: Quick Status Assessment
When taking over a new project or auditing an existing service, starting with browser extensions is the fastest approach.
| Tool | Purpose |
|---|---|
| axe DevTools (Chrome/Firefox extension) | Full page WCAG violation list with element highlighting |
| Lighthouse (built into Chrome DevTools) | Accessibility score with per-item descriptions |
| WAVE (WebAIM) | Visual accessibility report |
axe-core — An open-source accessibility detection engine by Deque Systems and the industry-standard tool. It can be integrated in various forms including jest-axe, cypress-axe, and axe DevTools.
Manual Screen Reader Testing
Flow issues that automated tools miss can only be caught by navigating with a screen reader yourself. Even with a Lighthouse score of 100, there are often awkward stretches when you actually use a screen reader to navigate.
| Tool | Platform | How to Start |
|---|---|---|
| VoiceOver | macOS / iOS (built-in) | Cmd + F5 |
| NVDA | Windows (free) | nvaccess.org |
| TalkBack | Android (built-in) | Settings > Accessibility |
At first, operating a screen reader feels unfamiliar and you'll want to turn it off within five minutes. Start by simply navigating the page with just the Tab key to check whether the focus order feels natural.
Pitfalls and Limitations to Watch Out For
Benefits
| Item | Details |
|---|---|
| Reduced legal risk | Defense against fines up to €1,260,000 and market exclusion risk for non-compliance |
| Expanded user base | Includes people with disabilities (approximately 26% of EU population), improves overall UX quality |
| SEO synergy | Semantic HTML, alt text, and clear structure also contribute to search engine crawling efficiency |
| Improved mobile quality | Touch target sizing and focus improvements also enhance usability for non-disabled users |
There's a good reason to align with designers on accessibility requirements early — fixing color contrast later can require overhauling the entire design system. After experiencing that once, I started always annotating contrast ratios at the token level.
Drawbacks and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Automation limitations | Tools detect only 40–57% of issues | Combine automation with manual screen reader testing |
| Legacy code debt | Existing component libraries need to be reviewed | Gradually migrate to accessibility-built-in libraries like Radix UI or React Aria |
| Design conflicts | Enhanced contrast and focus rings may conflict with existing design systems | Align with designers early, reflect contrast ratios at the design token level |
| Country-specific legal differences | Fine amounts and enforcement bodies vary by country | Verify local legal requirements per target country |
| Complex interactions | Accessibility implementation for carousels, modals, and drag-and-drop requires additional effort | Reference WAI-ARIA Authoring Practices Guide patterns |
Most Common Mistakes in Practice
- Applying
outline: noneglobally — Casually added at the design team's request, completely eliminating focus for keyboard users. Can be replaced with:focus-visible. - Overusing
aria-labelindiscriminately — ARIA attributes are a supplement for when semantic HTML can't solve the problem. Patching witharia-labelwhat could be solved with<label>causes screen readers to read text twice or makes maintenance more complex. - Skipping manual testing and relying only on automated tests — Even with a Lighthouse score of 100, the actual navigation flow is often awkward. It matters to press Tab with VoiceOver (
Cmd + F5) or NVDA.
Closing Thoughts
Accessibility is not "something to do later" — it's part of managing technical debt that you build up now. The EAA's enforcement creates legal obligations, but using semantic HTML and correct ARIA practices aligns precisely with the direction of improving code quality overall.
Three steps you can start right now:
- Start with an automated scan — Add the axe DevTools extension to Chrome or use the Lighthouse accessibility tab to get a picture of your current issues. Add
pnpm add -D axe-core jest-axeto your project and attach the snippet above to component tests to automatically catch regressions in CI. - Fix high-impact items first — Image alt text, form label connections, and
:focus-visiblefocus styles offer the best return on effort. Just addressing these three will produce a noticeable improvement. - Navigate with a screen reader for real — On macOS, press
Cmd + F5to turn on VoiceOver and navigate the page with the Tab key to directly experience flow issues that automated tools can't catch.
Today, navigating your own service with only a keyboard for the first time — that's where it begins.
Next article: Building accessible modal, dropdown, and carousel components directly using WAI-ARIA Authoring Practices (series link will be updated after publication)
References
- European Accessibility Act — European Commission Official Page
- EAA Complete Compliance Guide 2025 — AllAccessible
- EAA 2025 Compliance: Web Accessibility Explained — Coaxsoft
- EAA Fines and Penalties by Country 2025 — Web Accessibility Checker
- EAA Enforcement in Europe Following June 2025 Deadline — Pivotal Accessibility
- WCAG 2.2 Complete Guide 2025 — AllAccessible
- WCAG 2 Overview — W3C WAI
- axe-core GitHub — Deque Systems
- Accessibility Testing — Cypress Official Docs
- Korean Web Content Accessibility Guidelines (KWCAG) 2.2