Accessibility (a11y) Comprehensive accessibility guidelines based on WCAG 2.2 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities. WCAG Principles: POUR Principle Description P erceivable Content can be perceived through different senses O perable Interface can be operated by all users U nderstandable Content and interface are understandable R obust Content works with assistive technologies Conformance levels Level Requirement Target A Minimum accessibility Must pass AA Standard compliance Should pass (legal requirement in many jurisdictions) AAA Enhanced accessibility Nice to have Perceivable Text alternatives (1.1) Images require alt text:
< img src = " chart.png "
< img src = " chart.png " alt = " Bar chart showing 40% increase in Q3 sales "
< img src = " decorative-border.png " alt = " " role = " presentation "
< figure
< img src = " infographic.png " alt = " 2024 market trends infographic " aria-describedby = " infographic-desc "
< figcaption id = " infographic-desc "
</ figcaption
</ figure
Icon buttons need accessible names:
< button
< svg
</ svg
</ button
< button aria-label = " Open menu "
< svg aria-hidden = " true "
</ svg
</ button
< button
< svg aria-hidden = " true "
</ svg
< span class = " visually-hidden "
Open menu </ span
</ button
Visually hidden class: .visually-hidden { position : absolute ; width : 1 px ; height : 1 px ; padding : 0 ; margin : -1 px ; overflow : hidden ; clip : rect ( 0 , 0 , 0 , 0 ) ; white-space : nowrap ; border : 0 ; } Color contrast (1.4.3, 1.4.6) Text Size AA minimum AAA enhanced Normal text (< 18px / < 14px bold) 4.5:1 7:1 Large text (≥ 18px / ≥ 14px bold) 3:1 4.5:1 UI components & graphics 3:1 3:1 / ❌ Low contrast (2.5:1) / .low-contrast { color :
999
; background :
fff
; } / ✅ Sufficient contrast (7:1) / .high-contrast { color :
333
; background :
fff
; } / ✅ Focus states need contrast too / :focus-visible { outline : 2 px solid
005fcc
; outline-offset : 2 px ; } Don't rely on color alone:
< input class = " error-border "
< style
.error-border { border-color : red ; } </ style
< div class = " field-error "
< input aria-invalid = " true " aria-describedby = " email-error "
< span id = " email-error " class = " error-message "
< svg aria-hidden = " true "
</ svg
Please enter a valid email address </ span
</ div
Media alternatives (1.2)
< video controls
< source src = " video.mp4 " type = " video/mp4 "
< track kind = " captions " src = " captions.vtt " srclang = " en " label = " English " default
< track kind = " descriptions " src = " descriptions.vtt " srclang = " en " label = " Descriptions "
</ video
< audio controls
< source src = " podcast.mp3 " type = " audio/mp3 "
</ audio
< details
< summary
Transcript </ summary
< p
Full transcript text... </ p
</ details
Operable Keyboard accessible (2.1) All functionality must be keyboard accessible: // ❌ Only handles click element . addEventListener ( 'click' , handleAction ) ; // ✅ Handles both click and keyboard element . addEventListener ( 'click' , handleAction ) ; element . addEventListener ( 'keydown' , ( e ) => { if ( e . key === 'Enter' || e . key === ' ' ) { e . preventDefault ( ) ; handleAction ( ) ; } } ) ; No keyboard traps. Users must be able to Tab into and out of every component. Use the modal focus trap pattern for dialogs—the native
005fcc
; outline-offset : 2 px ; } / ✅ Or custom focus styles / button :focus-visible { box-shadow : 0 0 0 3 px rgba ( 0 , 95 , 204 , 0.5 ) ; } Focus not obscured (2.4.11) — new in 2.2 When an element receives keyboard focus, it must not be entirely hidden by other author-created content such as sticky headers, footers, or overlapping panels. At Level AAA (2.4.12), no part of the focused element may be hidden. / ✅ Account for sticky headers when scrolling to focused elements / :target { scroll-margin-top : 80 px ; } / ✅ Ensure focused items clear fixed/sticky bars / :focus { scroll-margin-top : 80 px ; scroll-margin-bottom : 60 px ; } Skip links (2.4.1) Provide a skip link so keyboard users can bypass repetitive navigation. See the skip link pattern for full markup and styles. Target size (2.5.8) — new in 2.2 Interactive targets must be at least 24 × 24 CSS pixels (AA). Exceptions: inline text links, elements where the browser controls the size, and targets where a 24px circle centered on the bounding box does not overlap another target. / ✅ Minimum target size / button , [ role = "button" ] , input [ type = "checkbox" ] + label , input [ type = "radio" ] + label { min-width : 24 px ; min-height : 24 px ; } / ✅ Comfortable target size (recommended 44×44) / .touch-target { min-width : 44 px ; min-height : 44 px ; display : inline-flex ; align-items : center ; justify-content : center ; } Dragging movements (2.5.7) — new in 2.2 Any action that requires dragging must have a single-pointer alternative (e.g., buttons, inputs). See the dragging movements pattern for a sortable-list example. Timing (2.2) // Allow users to extend time limits function showSessionWarning ( ) { const modal = createModal ( { title : 'Session Expiring' , content : 'Your session will expire in 2 minutes.' , actions : [ { label : 'Extend session' , action : extendSession } , { label : 'Log out' , action : logout } ] , timeout : 120000 } ) ; } Motion (2.3) / Respect reduced motion preference / @media ( prefers-reduced-motion : reduce ) { * , * ::before , * ::after { animation-duration : 0.01 ms !important ; animation-iteration-count : 1 !important ; transition-duration : 0.01 ms !important ; scroll-behavior : auto !important ; } } Understandable Page language (3.1.1)
< html
< html lang = " en "
< p
The French word for hello is < span lang = " fr "
bonjour </ span
. </ p
Consistent navigation (3.2.3)
< nav aria-label = " Main "
< ul
< li
< a href = " / " aria-current = " page "
Home </ a
</ li
< li
< a href = " /products "
Products </ a
</ li
< li
< a href = " /about "
About </ a
</ li
</ ul
</ nav
Consistent help (3.2.6) — new in 2.2 If a help mechanism (contact info, chat widget, FAQ link, self-help option) is repeated across multiple pages, it must appear in the same relative order each time. Users who rely on consistent placement shouldn't have to hunt for help on every page. Form labels (3.3.2) Every input needs a programmatically associated label. See the form labels pattern for explicit, implicit, and instructional examples. Error handling (3.3.1, 3.3.3) Announce errors to screen readers with role="alert" or aria-live , set aria-invalid="true" on invalid fields, and focus the first error on submit. See the error handling pattern for full markup and JS. Redundant entry (3.3.7) — new in 2.2 Don't force users to re-enter information they already provided in the same session. Auto-populate from earlier steps, or let users select from previously entered values. Exceptions: security re-confirmation and content that has expired.
< fieldset
< legend
Shipping address </ legend
< label
< input type = " checkbox " id = " same-as-billing " checked
Same as billing address </ label
</ fieldset
Accessible authentication (3.3.8) — new in 2.2 Login flows must not rely on cognitive function tests (e.g., remembering a password, solving a puzzle) unless at least one of: A copy-paste or autofill mechanism is available An alternative method exists (e.g., passkey, SSO, email link) The test uses object recognition or personal content (AA only; AAA removes this exception)
< input type = " password " id = " password " autocomplete = " current-password "
< button type = " button "
Sign in with passkey </ button
< button type = " button "
Email me a login link </ button
Robust ARIA usage (4.1.2) Prefer native elements:
< div role = " button " tabindex = " 0 "
Click me </ div
< button
Click me </ button
< div role = " checkbox " aria-checked = " false "
Option </ div
< label
< input type = " checkbox "
Option </ label
When ARIA is needed, use the correct roles and states. See the ARIA tabs pattern for a complete tablist example. Live regions (4.1.3) Use aria-live regions to announce dynamic content changes without moving focus. See the live regions pattern for markup and a showNotification() helper. Testing checklist Automated testing
Lighthouse accessibility audit
npx lighthouse https://example.com --only-categories
accessibility
axe-core
npm install @axe-core/cli -g axe https://example.com Manual testing Keyboard navigation: Tab through entire page, use Enter/Space to activate Screen reader: Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android) Zoom: Content usable at 200% zoom High contrast: Test with Windows High Contrast Mode Reduced motion: Test with prefers-reduced-motion: reduce Focus order: Logical and follows visual order Target size: Interactive elements meet 24×24px minimum See the screen reader commands reference for VoiceOver and NVDA shortcuts. Common issues by impact Critical (fix immediately) Missing form labels Missing image alt text Insufficient color contrast Keyboard traps No focus indicators Serious (fix before launch) Missing page language Missing heading structure Non-descriptive link text Auto-playing media Missing skip links Moderate (fix soon) Missing ARIA labels on icons Inconsistent navigation Missing error identification Timing without controls Missing landmark regions References WCAG 2.2 Quick Reference WAI-ARIA Authoring Practices Deque axe Rules Web Quality Audit WCAG criteria reference Accessibility code patterns