Skip to content
Back to Design Hub

Accessibility & WCAG 2.2 Compliance

Practical guide to building accessible web interfaces that meet WCAG 2.2 AA conformance.

AccessibilityBeginner

Why

Inaccessible interfaces exclude 15% of users with disabilities, create legal liability (ADA, EAA), and degrade SEO since search engines cannot parse ARIA-misused content. Accessibility is not optional.

The WebAIM Million 2024 report found 96.3% of homepages had detectable WCAG failures. ADA-related lawsuits exceeded 4,500 in 2023, making accessibility a legal and ethical necessity, not a nice-to-have.

When

Always. Accessibility must be built in from the first component, not retrofitted before launch. Audit existing UI immediately if it was built without a11y consideration.

How

1. Semantic HTML — use <button>, <nav>, <main>, <label> not <div onClick> 2. Keyboard navigation — every interactive element must be focusable and operable - Tab order follows visual order (no positive tabindex) - Enter/Space activates buttons, Escape closes dialogs 3. ARIA — only when semantic HTML is insufficient - aria-label for icon-only buttons - aria-expanded for collapsible sections - role="dialog" + aria-modal="true" for modals 4. Color contrast — minimum 4.5:1 for text, 3:1 for large text and UI components 5. Focus management — trap focus in modals, restore focus on close 6. Test — axe-core in CI, manual screen reader testing with NVDA/VoiceOver

Example

// Accessible modal dialog function Modal({ open, onClose, title, children }: ModalProps) { const dialogRef = useRef<HTMLDivElement>(null); useEffect(() => { if (!open) return; // Trap focus + restore on close const previouslyFocused = document.activeElement as HTMLElement; dialogRef.current?.focus(); const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); // Focus trap logic here... }; document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("keydown", handleKeyDown); previouslyFocused?.focus(); // restore focus }; }, [open, onClose]); if (!open) return null; return ( <div role="dialog" aria-modal="true" aria-labelledby="modal-title" ref={dialogRef} tabIndex={-1} className="fixed inset-0 z-50"> <h2 id="modal-title">{title}</h2> {children} <button onClick={onClose} aria-label="Close dialog">X</button> </div> ); }

Common Problems

  • ⚠Using role='button' on a <div> — should use a real <button> for keyboard support
  • ⚠aria-label overrides visible text — screen readers lose the button's text content
  • ⚠Positive tabindex (tabindex='1') — breaks natural tab order, use tabindex='-1' or '0'
  • ⚠Missing focus-visible styles — keyboard users cannot see where focus is

Best Practices

  • ✓Prefer semantic HTML over ARIA — ARIA is a last resort, not a first choice
  • ✓Run axe-core in CI — fail the build on new violations, not just warnings
  • ✓Test with real screen readers — automated tools catch 40% of issues at most
  • ✓Use prefers-reduced-motion media query — disable animations for vestibular sensitivity

FAQ

Do I need ARIA on every element for accessibility?

No — prefer semantic HTML (<button>, <nav>, <label>). ARIA is a last resort when HTML can't express the pattern. Overusing ARIA often makes things worse.

What WCAG 2.2 level should I target?

AA is the legal and practical baseline (ADA, EAA compliance). Aim for AA across the product, AAA on critical flows like checkout where feasible.

How do I test accessibility beyond automated tools?

axe-core catches ~40% of issues. Test the rest with a real screen reader (NVDA/VoiceOver) and keyboard-only navigation — every interactive element must be reachable and operable.

What color contrast ratio do I need?

4.5:1 for normal text, 3:1 for large text (≥18px or 14px bold) and UI components. Use a contrast checker in your design tool, not just eyeballing.

How do I handle focus management in modals?

Trap focus inside the modal while open, restore focus to the trigger on close. Use aria-modal='true' and listen for Escape to close.