Files
2025-11-30 08:58:02 +08:00

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 tabindex values > 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:

Patterns: