12 KiB
Accessibility Guidelines
WCAG 2.1 Level AA compliance checklist and best practices for web design.
Color & Contrast
Text Contrast Requirements
Normal text (< 18px or < 14px bold):
- Minimum contrast ratio: 4.5:1 against background
- Example: #18181b text on #ffffff = 19.56:1 ✓
- Example: #71717a text on #fafafa = 2.8:1 ✗
Large text (≥ 18px or ≥ 14px bold):
- Minimum contrast ratio: 3:1 against background
- Recommended: Still aim for 4.5:1 when possible
UI Components & Graphics:
- Interactive elements: 3:1 against adjacent colors
- Graphs, charts, icons: 3:1 minimum
- Focus indicators: 3:1 against background
Tools for Testing
Use these tools to verify contrast:
- WebAIM Contrast Checker
- Chrome DevTools Lighthouse
- Stark plugin (Figma/Sketch)
- Contrast Analyzer (desktop app)
Common Issues
Insufficient contrast:
- Light gray text on white (#aaa on #fff = 2.3:1)
- Placeholder text often fails (many browsers use low contrast)
- Disabled states (okay to have lower contrast, but clearly indicate disabled)
Good practices:
- Text on images: Add overlay or shadow for contrast
- Links: Underline or sufficient contrast difference
- Buttons: Ensure text contrasts with background
Semantic HTML
Use Appropriate Elements
Navigation:
<nav>
<ul>
<li><a href="/">Home</a></li>
</ul>
</nav>
Main content:
<main>
<article>
<h1>Page Title</h1>
<p>Content...</p>
</article>
</main>
Complementary content:
<aside>
<h2>Related Links</h2>
</aside>
Page sections:
<section>
<h2>Section Title</h2>
</section>
Buttons vs Links:
<button>: Actions (submit, toggle, trigger)<a>: Navigation to another page/location
Heading Hierarchy
Rules:
- One
<h1>per page (page title) - Don't skip levels (h1 → h3 is wrong)
- Headings create document outline
Good structure:
<h1>Main Title</h1>
<h2>Section 1</h2>
<h3>Subsection 1.1</h3>
<h3>Subsection 1.2</h3>
<h2>Section 2</h2>
Lists
Use lists for:
- Navigation menus
- Steps/sequences
- Related items
- Features/benefits
Types:
<ul>: Unordered (bullets)<ol>: Ordered (numbers)<dl>: Definition lists (term/description pairs)
Keyboard Navigation
Focus Management
All interactive elements must be keyboard accessible:
- Links (
<a>) - Buttons (
<button>) - Form inputs
- Custom interactive elements (add tabindex="0")
Focus indicators must be visible:
button:focus-visible {
outline: 2px solid #0ea5e9;
outline-offset: 2px;
}
Don't remove default focus without replacement:
/* BAD */
*:focus { outline: none; }
/* GOOD */
*:focus { outline: 2px solid #0ea5e9; }
Tab Order
Natural DOM order is best:
- Don't use
tabindexvalues > 0 (breaks natural flow) - Use
tabindex="-1"to remove from tab order when appropriate - Use
tabindex="0"to add custom elements to tab order
Skip links for long navigation:
<a href="#main-content" class="skip-link">
Skip to main content
</a>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: #000;
color: #fff;
padding: 8px;
}
.skip-link:focus {
top: 0;
}
Keyboard Shortcuts
Essential interactions:
- Tab: Move forward through interactive elements
- Shift+Tab: Move backward
- Enter: Activate links/buttons
- Space: Activate buttons, checkboxes
- Escape: Close modals/dropdowns
- Arrow keys: Navigate within components (tabs, menus, sliders)
Custom interactions: Document any custom keyboard shortcuts clearly in UI.
Screen Readers
Alternative Text
Images:
<!-- Informative image -->
<img src="chart.png" alt="Bar chart showing 60% increase in revenue">
<!-- Decorative image -->
<img src="decorative-border.png" alt="">
<!-- Image as link -->
<a href="/products">
<img src="logo.png" alt="Acme Products Homepage">
</a>
Icons:
<!-- Functional icon with text -->
<button>
<svg aria-hidden="true">...</svg>
<span>Save</span>
</button>
<!-- Functional icon without visible text -->
<button aria-label="Save document">
<svg aria-hidden="true">...</svg>
</button>
<!-- Decorative icon -->
<span aria-hidden="true">★</span>
ARIA Labels
Form inputs:
<!-- Visible label (preferred) -->
<label for="email">Email Address</label>
<input id="email" type="email">
<!-- ARIA label when visible label not possible -->
<input type="search" aria-label="Search products" placeholder="Search...">
Buttons:
<!-- Text describes action (no ARIA needed) -->
<button>Submit Application</button>
<!-- Icon-only button needs label -->
<button aria-label="Close dialog">
<svg>...</svg>
</button>
Navigation landmarks:
<nav aria-label="Primary navigation">...</nav>
<nav aria-label="Footer navigation">...</nav>
Live Regions
Dynamic content updates:
<!-- Polite: announce when user is idle -->
<div aria-live="polite" aria-atomic="true">
<p>5 new messages</p>
</div>
<!-- Assertive: announce immediately -->
<div role="alert" aria-live="assertive">
<p>Error: Failed to save changes</p>
</div>
<!-- Status: for status messages -->
<div role="status" aria-live="polite">
<p>Saving...</p>
</div>
Loading states:
<button aria-busy="true" aria-label="Loading, please wait">
<span class="spinner" aria-hidden="true"></span>
Loading...
</button>
Forms
Labels & Instructions
Every input needs a label:
<!-- Explicit label (preferred) -->
<label for="username">Username</label>
<input id="username" type="text">
<!-- Implicit label -->
<label>
Username
<input type="text">
</label>
Required fields:
<label for="email">
Email Address
<span aria-label="required">*</span>
</label>
<input id="email" type="email" required aria-required="true">
Helper text:
<label for="password">Password</label>
<input id="password"
type="password"
aria-describedby="password-hint">
<div id="password-hint">
Must be at least 8 characters
</div>
Error Handling
Validation errors:
<label for="email">Email Address</label>
<input id="email"
type="email"
aria-invalid="true"
aria-describedby="email-error">
<div id="email-error" role="alert">
Please enter a valid email address
</div>
Error summary:
<div role="alert" aria-labelledby="error-heading">
<h2 id="error-heading">There are 2 errors in this form</h2>
<ul>
<li><a href="#email">Email address is required</a></li>
<li><a href="#password">Password must be at least 8 characters</a></li>
</ul>
</div>
Fieldsets & Groups
Related inputs:
<fieldset>
<legend>Shipping Address</legend>
<label for="street">Street</label>
<input id="street" type="text">
<label for="city">City</label>
<input id="city" type="text">
</fieldset>
Radio button groups:
<fieldset>
<legend>Select your plan</legend>
<label>
<input type="radio" name="plan" value="basic">
Basic
</label>
<label>
<input type="radio" name="plan" value="pro">
Pro
</label>
</fieldset>
Interactive Components
Buttons
Button requirements:
- Minimum size: 44x44px (iOS guideline)
- Clear focus indicator
- Disabled state clearly visible
- Loading state announced to screen readers
<!-- Primary action -->
<button type="button">Save Changes</button>
<!-- Disabled -->
<button type="button" disabled aria-disabled="true">
Save Changes
</button>
<!-- Loading -->
<button type="button" aria-busy="true" aria-label="Saving, please wait">
<span class="spinner" aria-hidden="true"></span>
Saving...
</button>
Modals/Dialogs
Modal requirements:
- Focus trap (keep focus inside modal)
- Close with Escape key
- Return focus to trigger element on close
- Screen readers announce modal opening
<div role="dialog"
aria-modal="true"
aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm Action</h2>
<p>Are you sure you want to delete this item?</p>
<button type="button">Cancel</button>
<button type="button">Delete</button>
</div>
Dropdowns/Menus
Menu button pattern:
<button aria-haspopup="true"
aria-expanded="false"
aria-controls="menu">
Options
</button>
<ul id="menu" role="menu">
<li role="menuitem">Edit</li>
<li role="menuitem">Delete</li>
</ul>
Tabs
Tab pattern:
<div role="tablist" aria-label="Project details">
<button role="tab"
aria-selected="true"
aria-controls="overview-panel">
Overview
</button>
<button role="tab"
aria-selected="false"
aria-controls="activity-panel">
Activity
</button>
</div>
<div id="overview-panel" role="tabpanel">
Overview content...
</div>
<div id="activity-panel" role="tabpanel" hidden>
Activity content...
</div>
Mobile & Touch
Touch Targets
Minimum sizes:
- 44x44px on iOS (Apple guideline)
- 48x48px on Android (Material Design)
- Use larger targets for primary actions
Spacing:
- 8px minimum between touch targets
- More spacing for dense interfaces
Viewport & Zoom
Allow zoom:
<!-- Good -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bad - don't prevent zoom -->
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
Responsive text:
- Use relative units (rem, em)
- Don't set maximum font size
- Ensure text reflows at 200% zoom
Testing Checklist
Automated Testing
- Run Lighthouse accessibility audit
- Check WAVE browser extension
- Validate HTML (W3C validator)
- Test color contrast (WebAIM checker)
Manual Testing
- Navigate entire site using only keyboard
- Test with screen reader (NVDA, JAWS, VoiceOver)
- Zoom to 200% and verify layout
- Test with browser extensions disabled
- Test on mobile device
- Test with reduced motion settings
- Test in high contrast mode
Specific Checks
- All images have alt text
- Forms have proper labels
- Focus indicators are visible
- Color is not only method of conveying info
- Text has sufficient contrast
- Headings are properly nested
- Links have descriptive text
- Videos have captions
- Audio has transcripts
- Tables have proper headers
- Interactive elements are keyboard accessible
- Error messages are clear and helpful
- Loading states are announced
- Modals trap focus and close with Escape
Common Mistakes
Don't:
- Use
<div>or<span>as buttons (use<button>) - Remove focus indicators without replacements
- Use color alone to convey meaning
- Disable zoom on mobile
- Skip heading levels
- Use placeholder as label
- Make click targets too small
- Forget alt text on images
- Use ambiguous link text ("click here")
- Prevent keyboard access to functionality
Do:
- Use semantic HTML elements
- Provide clear focus indicators
- Label all form inputs
- Make touch targets 44x44px minimum
- Test with keyboard and screen reader
- Provide alternatives for non-text content
- Write descriptive link text
- Announce dynamic content changes
- Support keyboard navigation patterns
- Document accessibility features
Resources
Testing tools:
- Chrome DevTools Lighthouse
- WAVE (Web Accessibility Evaluation Tool)
- axe DevTools
- Screen readers: NVDA (Windows), JAWS (Windows), VoiceOver (Mac/iOS)
Guidelines:
- WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/
- MDN Accessibility: https://developer.mozilla.org/en-US/docs/Web/Accessibility
- WebAIM: https://webaim.org/
Patterns:
- ARIA Authoring Practices: https://www.w3.org/WAI/ARIA/apg/
- Inclusive Components: https://inclusive-components.design/