WCAG 2.2 Practical Guide: Web Accessibility Compliance for Frontend Developers
When your Lighthouse accessibility score hits the 40s, you've probably heard your team lead say something like: "We'll deal with accessibility later — features come first." I went through the same thing repeatedly, until one day I was handed the task of retrofitting accessibility onto a legacy codebase. The refactoring effort turned out to be three times the cost of feature development. That experience taught me firsthand just how expensive it gets when you don't bake it in from the design stage.
But "later" no longer flies in today's environment. The EU's European Accessibility Act (EAA) came into effect on June 28, 2025, and domestically, the Digital Inclusion Act is scheduled to take effect in January 2026. The Ministry of the Interior and Safety has set a 2026 deadline for e-government websites to meet KWCAG 2.2, and in the United States alone, 4,605 federal accessibility lawsuits were filed in 2024. The era of pushing accessibility to the last checkbox on a QA checklist is over.
This article covers accessibility patterns that frontend developers can apply directly in practice — from core WCAG 2.2 criteria to focus trap implementation, color contrast ratios, and setting up @axe-core/playwright automation. You've already seen plenty of regulatory news, so I'll focus on the hands-on, code-level details.
Core Concepts
The POUR Principles and Conformance Levels
The international standard for web accessibility is the WCAG (Web Content Accessibility Guidelines), published by W3C. The current latest version is WCAG 2.2, released in October 2023. In Korea, the localized KWCAG 2.2 applies it across 4 principles, 14 guidelines, and 33 checkpoints.
The backbone of WCAG is the four POUR principles.
| Principle | Description |
|---|---|
| Perceivable | Information and UI components must be presentable to users in ways they can perceive (alternative text, captions, etc.) |
| Operable | All functionality must be operable through keyboards and other diverse input methods |
| Understandable | Information and UI behavior must be understandable |
| Robust | Content must be interpreted by a wide variety of user agents, including assistive technologies like screen readers |
Conformance Levels have three tiers: A (minimum), AA (standard), and AAA (enhanced). Since Korea's Anti-Discrimination Act, the EU EAA, and the US ADA all require Level AA compliance, AA is the baseline to target.
New WCAG 2.2 Criteria That Directly Affect Frontend Development
Moving from WCAG 2.1 to 2.2, nine new success criteria were added. Here are the key items that require code changes.
| Criterion | Level | Key Requirement |
|---|---|---|
| 2.4.11 Focus Not Obscured (Minimum) | AA | A focused component must not be entirely hidden by other content (watch out for sticky headers) |
| 2.4.13 Focus Appearance | AA | Focus indicators must be at least 2px thick with a contrast ratio of at least 3:1 |
| 2.5.7 Dragging Movements | AA | Functionality that requires dragging must offer a single-pointer alternative |
| 2.5.8 Target Size (Minimum) | AA | Touch targets must be at least 24×24 CSS pixels |
| 3.3.8 Accessible Authentication (Minimum) | AA | Authentication steps must not require cognitive function tests (e.g., CAPTCHA puzzles) |
| 3.3.7 Redundant Entry | A | Users must not be required to re-enter information already provided in the same session |
Practical Application
Restoring Keyboard Navigation Broken by outline: none (2.4.13 Focus Appearance)
Open the reset.css of many projects and you'll find something like this:
* {
outline: none; /* Remove browser default focus styles */
}This one line — added because the default didn't match the design — completely blocks keyboard users from navigating, since there's no visual indication of where focus is. Using :focus-visible, you can make the focus ring invisible on mouse clicks while still showing it during keyboard navigation. It's an approach that's easy to sell to design teams, which made it easier to get buy-in in practice.
/* Replace outline: none in reset.css with this */
:focus-visible {
outline: 3px solid #005FCC;
outline-offset: 2px;
border-radius: 2px;
}| Property | Reason |
|---|---|
3px solid |
Meets WCAG 2.4.13's 2px thickness requirement while ensuring visibility |
outline-offset: 2px |
Adds slight spacing from the element border for improved readability |
:focus-visible |
No focus ring on mouse click; shows only for keyboard focus |
The focus indicator color must have a contrast ratio of at least 3:1 against the background to pass WCAG 2.4.13. #005FCC (blue) against a white background has a contrast ratio of approximately 6.5:1, well above the threshold.
With focus styles sorted, it's time to look at layout-level issues.
Fixing Sticky Header Focus Conflicts (2.4.11 Focus Not Obscured)
In layouts with a sticky header, tabbing through the page often results in focused elements being hidden behind the header. WCAG 2.4.11 was added specifically because of this problem. It can be solved simply with scroll-margin-top, but hardcoding the header height will break when the layout changes — managing it with a CSS variable is the better approach.
:root {
--header-height: 64px;
}
:focus-visible {
scroll-margin-top: var(--header-height, 80px);
}By managing the header height in a single --header-height variable, you only need to update the variable value inside a @media query when the header size changes responsively.
Ensuring 44px Touch Targets (2.5.8 Target Size)
Even visually small elements like icon buttons need sufficiently large touch areas. The WCAG 2.2 minimum is 24×24px, but iOS HIG recommends 44×44px — meeting both standards at once.
.icon-btn {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}This approach keeps the icon itself at 20–24px while expanding only the clickable area, so you meet the accessibility requirement without touching the visual design.
Meeting Text and Non-Text Color Contrast Requirements (1.4.3, 1.4.11)
It's common to address only the focus indicator contrast ratio (3:1) and overlook text contrast. Since color contrast accounts for a significant portion of accessibility issues, it helps to have all the thresholds in one place.
| Target | Minimum Contrast Ratio | Standard |
|---|---|---|
| Normal text (below 18pt) | 4.5:1 | WCAG 1.4.3 |
| Large text (18pt or above, or bold 14pt or above) | 3:1 | WCAG 1.4.3 |
| UI component borders and icons | 3:1 | WCAG 1.4.11 |
| Focus indicators | 3:1 | WCAG 2.4.13 |
Documenting these thresholds alongside your design system color tokens creates a shared language when discussing accessibility tokens with designers. Our team started annotating Figma tokens with contrast ratio values in the format "this color combination passes AA," and accessibility issues caught during review dropped noticeably after that. For checking contrast, WebAIM Contrast Checker or the color picker in Chrome DevTools works well.
With color contrast handled, let's move on to modals — the most complex case among dynamic UIs.
Completing Modal Accessibility — Including Focus Traps (WAI-ARIA)
Modals are among the trickiest components to get right for accessibility. It's tempting to think that just adding role="dialog" and aria-modal is enough, but that alone still lets keyboard users Tab out to elements behind the modal. A focus trap implementation is absolutely required.
<!-- Modal markup -->
<button id="open-modal-btn">Purchase</button>
<div
id="purchase-modal"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc"
hidden
>
<h2 id="dialog-title">Confirm Purchase</h2>
<p id="dialog-desc">Would you like to purchase the selected item?</p>
<button id="confirm-btn">Confirm</button>
<button id="cancel-btn">Cancel</button>
</div>const modal = document.getElementById('purchase-modal');
const trigger = document.getElementById('open-modal-btn');
const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
function openModal() {
modal.removeAttribute('hidden');
// Move focus to the first focusable element when modal opens
modal.querySelector(FOCUSABLE)?.focus();
document.addEventListener('keydown', handleKeyDown);
}
function closeModal() {
modal.setAttribute('hidden', '');
document.removeEventListener('keydown', handleKeyDown);
// Return focus to the trigger button when modal closes
trigger.focus();
}
function handleKeyDown(e) {
if (e.key === 'Escape') {
closeModal();
return;
}
if (e.key !== 'Tab') return;
// Focus trap: Tab from last element → first; Shift+Tab from first → last
const focusable = [...modal.querySelectorAll(FOCUSABLE)];
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
trigger.addEventListener('click', openModal);
document.getElementById('cancel-btn').addEventListener('click', closeModal);In practice, it's far better to use headless libraries like Radix UI or React Aria rather than implementing this logic from scratch. Treat the code above as a reference for understanding the underlying mechanics, and delegate to a well-tested library in production.
WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications) is a W3C specification for conveying the roles, states, and properties of dynamic content — which can't be expressed through HTML semantics alone — to assistive technologies. It consists of
roleandaria-*attributes.
Headless UI libraries: Collections of components with WAI-ARIA patterns and keyboard interactions pre-implemented, but without any styling. Radix UI, React Aria (Adobe), and Headless UI (Tailwind Labs) are leading examples that significantly reduce the cost of implementing accessibility.
Image alt Attributes and Live Feedback (aria-live)
Alt text is not "a description of an image" — it's a replacement for the information the image conveys.
<!-- Informative image: describe the data the image itself carries -->
<img src="chart.png" alt="Q1 2025 revenue: up 23% year-over-year" />
<!-- Decorative image: alt="" alone causes screen readers to ignore it -->
<img src="divider.png" alt="" />For decorative images, alt="" alone is already sufficient for screen readers to ignore them. Adding role="presentation" alongside it is redundant and unnecessary.
For status messages that change dynamically — like form submission or save completion — aria-live lets screen readers announce the update immediately.
<div aria-live="polite" aria-atomic="true" id="status-message">
<!-- Screen reader automatically reads content injected here via JavaScript -->
</div>function showStatus(message) {
document.getElementById('status-message').textContent = message;
}
// On successful form submission
showStatus('Your changes have been saved.');Automating Accessibility in CI with @axe-core/playwright
Honestly, manual testing alone can't catch every issue. Adding @axe-core/playwright to your E2E tests lets you automatically flag WCAG violations.
A quick note on the package name: axe-playwright is a third-party wrapper — the official package is @axe-core/playwright. Installing axe-playwright gets you a different package.
import AxeBuilder from '@axe-core/playwright';
import { test, expect } from '@playwright/test';
test('Homepage accessibility check', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});| Tool | Environment |
|---|---|
| jest-axe | Jest unit tests |
| @axe-core/playwright | Playwright E2E |
| @axe-core/react | React development runtime warnings |
| Cypress-axe | Cypress E2E |
That said, be aware that automated tools detect only about 40% of all accessibility issues. The remaining 60% requires manual testing with screen readers like NVDA (Windows) or VoiceOver (macOS/iOS).
Pros and Cons
Advantages
| Item | Details |
|---|---|
| Reduced legal risk | Proactively mitigates the risk of domestic and international lawsuits, administrative sanctions, and fines |
| SEO improvement | Semantic markup, alternative text, and structured headings directly contribute to search engine crawling |
| Better usability | Keyboard navigation, clear labels, and sufficient color contrast improve the experience for non-disabled users as well |
| Expanded market | Approximately 2.65 million people with disabilities in Korea; the potential user base grows significantly when elderly users are included |
| Easier global expansion | Compliance with global regulations like EAA and ADA lowers the barrier to launching services internationally |
Disadvantages and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Legacy code debt | Retroactively applying accessibility to older codebases requires significant refactoring effort | Apply to new components first and expand incrementally |
| Automation limitations | Tools detect only about 40% of all issues; the rest requires manual testing | Supplement with direct screen reader testing using NVDA, VoiceOver, etc. |
| Design conflicts | Focus indicator and color contrast requirements may clash with brand design guidelines | Bake accessibility standards into design tokens at the design system stage |
| Dynamic content complexity | SPAs, infinite scroll, modals, and other dynamic UIs make ARIA state management complex | Use accessibility-first headless libraries like Radix UI or React Aria |
| Awareness gaps within teams | The practice of deferring accessibility to the final QA stage is still common | Including accessibility criteria in your sprint Definition of Done helps |
The Most Common Mistakes in Practice
- Applying
outline: noneglobally — Removing all focus styles inreset.csscompletely blocks keyboard users from navigating. Replace with:focus-visible-based styles. - Misusing positive
tabindexvalues — Values liketabindex="2"ortabindex="5"break the alignment between DOM order and visual order, making Tab navigation unpredictable. Use onlytabindex="0"or-1. - Indicating error state with color alone — Using only a red border to signal an error means users with color blindness won't perceive it. Always pair it with an icon and a text message.
- Redundantly declaring
role="presentation"on decorative images —alt=""alone is already sufficient for screen readers to ignore the element. Addingrole="presentation"is unnecessary. - Missing focus traps in modals — Declaring
role="dialog"alone still allows keyboard users to Tab to elements behind the modal. A focus trap implementation is mandatory.
Closing Thoughts
The longer you defer accessibility, the more the cost compounds exponentially. I learned firsthand the difference between spending 30 minutes at the design stage and retrofitting a legacy codebase after the fact. If convincing your team lead is difficult, framing it as "improving the Lighthouse accessibility score directly impacts SEO" tends to be far more persuasive than "litigation risk after 2026." Connecting it to product quality and business metrics moves organizations more effectively than framing it as a legal obligation.
Three steps you can take right now:
- Establish your current baseline — Open Lighthouse in Chrome DevTools and check your Accessibility score. The WAVE browser extension is also handy for visually highlighting problem locations.
- Add @axe-core/playwright to CI — Add it to your existing Playwright test setup. CI will catch it the moment new code breaks an accessibility standard.
- Restore focus styles and introduce Radix UI for new components — Replace
outline: noneinreset.csswith:focus-visible-based styles, and build new modals and dropdowns with Radix UI. You'll notice a significant reduction in the burden of accessibility implementation.
Next article: Deep dive into WAI-ARIA patterns — practical patterns for building screen reader-friendly dynamic UIs including tabs, dropdowns, infinite scroll, and live regions.
References
- Korean Web Content Accessibility Guidelines (KWCAG) 2.2
- Web Content Accessibility Guidelines 2.2 Korean Translation
- WCAG 2.2 Official Documentation — W3C
- What's New in WCAG 2.2 — W3C WAI
- Act on the Prohibition of Discrimination against Persons with Disabilities — National Law Information Center
- Accessibility Accommodations for Kiosks and Mobile Apps — Ministry of Health and Welfare
- ADA Web Accessibility Rule Fact Sheet 2024 — ADA.gov
- European Accessibility Act Compliance Guide — AllAccessible
- WAI-ARIA Authoring Practices 1.2 Korean
- ARIA — MDN Web Docs
- Accessibility Testing by Developers — Naver NULI
- Accessible Web Testing with Playwright and axe-core — DEV Community
- Headless UI Library Comparison — LogRocket