Accessibility Implementation

1. Semantic HTML for Screen Readers

Semantic Element Purpose Screen Reader Benefit Replaces
<header> Page/section header Announces "banner" or "header" landmark <div class="header">
<nav> Navigation section Announces "navigation" landmark, quick access <div class="nav">
<main> Primary content Announces "main" landmark, skip-to-content <div class="main">
<article> Self-contained content Announces article boundary, easier navigation <div class="article">
<section> Thematic content grouping Announces section, hierarchical structure <div class="section">
<aside> Sidebar/tangential content Announces "complementary" landmark <div class="sidebar">
<footer> Page/section footer Announces "contentinfo" or "footer" landmark <div class="footer">
Heading Level Purpose Rules
<h1> Page title/main heading One per page (ideally), highest level
<h2> Major section headings Direct children of h1 sections
<h3> - <h6> Subsection headings Don't skip levels (h2 → h4 ❌)
List Type When to Use Screen Reader Announcement
<ul> Unordered list (bullets) "List, X items"
<ol> Ordered list (numbered) "List, X items" + item numbers
<dl> Definition/description list Announces term-definition pairs

Example: Semantic page structure for screen readers

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Accessible Page Example</title>
</head>
<body>
  <!-- Skip to main content link (hidden but accessible) -->
  <a href="#main-content" class="skip-link">Skip to main content</a>
  
  <!-- Header landmark -->
  <header>
    <h1>Website Title</h1>
    <p>Tagline or description</p>
  </header>
  
  <!-- Navigation landmark -->
  <nav aria-label="Main navigation">
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/about">About</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
  </nav>
  
  <!-- Main landmark (only one per page) -->
  <main id="main-content">
    <!-- Article 1 -->
    <article>
      <h2>Article Title</h2>
      <p>Published on <time datetime="2025-12-22">December 22, 2025</time></p>
      
      <section>
        <h3>Introduction</h3>
        <p>Article introduction...</p>
      </section>
      
      <section>
        <h3>Main Content</h3>
        <p>Article content...</p>
        
        <section>
          <h4>Subsection</h4>
          <p>Subsection content...</p>
        </section>
      </section>
    </article>
    
    <!-- Article 2 -->
    <article>
      <h2>Another Article</h2>
      <p>Content...</p>
    </article>
  </main>
  
  <!-- Complementary landmark -->
  <aside aria-label="Sidebar">
    <section>
      <h2>Related Links</h2>
      <ul>
        <li><a href="/related1">Related Article 1</a></li>
        <li><a href="/related2">Related Article 2</a></li>
      </ul>
    </section>
  </aside>
  
  <!-- Contentinfo landmark -->
  <footer>
    <p>&copy; 2025 Company Name. All rights reserved.</p>
    <nav aria-label="Footer navigation">
      <ul>
        <li><a href="/privacy">Privacy Policy</a></li>
        <li><a href="/terms">Terms of Service</a></li>
      </ul>
    </nav>
  </footer>
</body>
</html>
Best Practices: Use semantic elements instead of divs where possible. Maintain logical heading hierarchy (don't skip levels). One <main> per page. Label multiple navs with aria-label. Use <time> for dates with datetime attribute. Provide skip-to-content links for keyboard users.

2. ARIA Roles, States, and Properties

ARIA Category Purpose Examples
Roles Define element type/purpose button, dialog, tablist, alert
States Current condition (can change) aria-checked, aria-expanded, aria-pressed
Properties Characteristics (rarely change) aria-label, aria-labelledby, aria-describedby
Common ARIA Role Use When Required ARIA Attributes
role="button" Clickable non-button element tabindex="0", keyboard handlers
role="dialog" Modal or dialog window aria-labelledby or aria-label
role="alert" Important, time-sensitive message None (auto-announced)
role="tablist" Tab navigation component With role="tab" and role="tabpanel"
role="navigation" Navigation links Use <nav> aria-label (if multiple navs)
role="search" Search form None
role="status" Status message (low priority) None (politely announced)
role="progressbar" Progress indicator aria-valuenow, aria-valuemin, aria-valuemax
ARIA Property Purpose Example
aria-label Provide accessible name aria-label="Close dialog"
aria-labelledby Reference element ID for label aria-labelledby="dialog-title"
aria-describedby Reference element ID for description aria-describedby="hint-text"
aria-hidden Hide from screen readers aria-hidden="true" (decorative icons)
aria-live Announce dynamic content aria-live="polite" or "assertive"
aria-expanded Collapsible element state aria-expanded="false"
aria-pressed Toggle button state aria-pressed="true"
aria-checked Checkbox/radio state aria-checked="true"
aria-selected Selection state (tabs, options) aria-selected="true"
aria-disabled Disabled state (still focusable) aria-disabled="true"
aria-current Current item in set aria-current="page" or "step"

Example: Custom button with ARIA

<!-- Native button (preferred) -->
<button type="button">Click Me</button>

<!-- Custom button with ARIA (if native not possible) -->
<div 
  role="button" 
  tabindex="0"
  aria-label="Close dialog"
  onclick="closeDialog()"
  onkeydown="handleKeyPress(event)">
  <span aria-hidden="true">×</span>
</div>

<script>
function handleKeyPress(e) {
  // Space or Enter activates button
  if (e.key === ' ' || e.key === 'Enter') {
    e.preventDefault();
    closeDialog();
  }
}
</script>

Example: Accessible tabs with ARIA

<div class="tabs">
  <!-- Tab list -->
  <div role="tablist" aria-label="Content tabs">
    <button 
      role="tab" 
      id="tab-1"
      aria-selected="true" 
      aria-controls="panel-1"
      tabindex="0">
      Tab 1
    </button>
    <button 
      role="tab" 
      id="tab-2"
      aria-selected="false" 
      aria-controls="panel-2"
      tabindex="-1">
      Tab 2
    </button>
    <button 
      role="tab" 
      id="tab-3"
      aria-selected="false" 
      aria-controls="panel-3"
      tabindex="-1">
      Tab 3
    </button>
  </div>
  
  <!-- Tab panels -->
  <div 
    role="tabpanel" 
    id="panel-1" 
    aria-labelledby="tab-1"
    tabindex="0">
    Content for tab 1
  </div>
  
  <div 
    role="tabpanel" 
    id="panel-2" 
    aria-labelledby="tab-2"
    hidden
    tabindex="0">
    Content for tab 2
  </div>
  
  <div 
    role="tabpanel" 
    id="panel-3" 
    aria-labelledby="tab-3"
    hidden
    tabindex="0">
    Content for tab 3
  </div>
</div>

Example: Live regions for dynamic updates

<!-- Polite announcement (waits for pause) -->
<div role="status" aria-live="polite" aria-atomic="true">
  <!-- Content updated via JavaScript -->
  <p>5 new messages</p>
</div>

<!-- Assertive announcement (interrupts) -->
<div role="alert" aria-live="assertive">
  <!-- Urgent notifications -->
  <p>Error: Connection lost</p>
</div>

<!-- Loading state -->
<div 
  role="status" 
  aria-live="polite" 
  aria-busy="true"
  aria-label="Loading content">
  <span class="spinner"></span>
  Loading...
</div>

<script>
// Update live region
function updateStatus(message) {
  const status = document.querySelector('[role="status"]');
  status.textContent = message;
  // Screen reader announces: "5 new messages"
}

// Show error
function showError(error) {
  const alert = document.querySelector('[role="alert"]');
  alert.textContent = error;
  // Screen reader immediately announces error
}
</script>
ARIA Rules: First rule: Don't use ARIA if native HTML element exists (use <button> not <div role="button">). Second rule: Don't change native semantics (don't add role to <h1>). ARIA doesn't provide behavior - you must add keyboard handlers and focus management. Test with actual screen readers.

3. Keyboard Navigation and Focus Management

Keyboard Action Expected Behavior Elements
Tab Move focus to next focusable element All interactive elements
Shift + Tab Move focus to previous focusable element All interactive elements
Enter Activate element (click) Links, buttons, form submissions
Space Activate button, check checkbox Buttons, checkboxes
Arrow Keys Navigate within component Radio groups, tabs, menus, sliders
Escape Close/cancel Dialogs, menus, dropdowns
Home / End Jump to first/last item Lists, sliders, combo boxes
tabindex Value Behavior Use Case
tabindex="0" Natural tab order, focusable Make div/span focusable (custom controls)
tabindex="-1" Not in tab order, programmatically focusable Headings for skip links, modal content
tabindex="1+" Explicit order AVOID ❌ Breaks natural order, causes confusion
No tabindex Native focusable elements only Default for button, input, a, etc.
Focus Pattern Implementation Use Case
Focus Trap Cycle Tab within container Modal dialogs, dropdowns
Focus Restoration Return focus to trigger element Close modal, dropdown, menu
Skip Links Jump to main content Bypass navigation, headers
Roving Tabindex One focusable item in group, arrows move Radio groups, toolbars, tabs

Example: Skip to main content link

<style>
  .skip-link {
    position: absolute;
    top: -40px;
    left: 0;
    background: #000;
    color: #fff;
    padding: 8px;
    z-index: 100;
  }
  
  .skip-link:focus {
    top: 0;
  }
</style>

<!-- Visible on focus only -->
<a href="#main-content" class="skip-link">
  Skip to main content
</a>

<header>...navigation...</header>

<main id="main-content" tabindex="-1">
  <h1>Page Title</h1>
  <!-- Content -->
</main>

Example: Focus trap in modal dialog

<div 
  role="dialog" 
  aria-labelledby="dialog-title"
  aria-modal="true"
  class="modal">
  
  <h2 id="dialog-title">Confirm Action</h2>
  <p>Are you sure you want to continue?</p>
  
  <button id="confirm-btn">Confirm</button>
  <button id="cancel-btn">Cancel</button>
</div>

<script>
class ModalDialog {
  constructor(element) {
    this.modal = element;
    this.focusableElements = this.modal.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    this.firstFocusable = this.focusableElements[0];
    this.lastFocusable = this.focusableElements[this.focusableElements.length - 1];
    this.previousFocus = null;
  }
  
  open() {
    // Store current focus
    this.previousFocus = document.activeElement;
    
    // Show modal
    this.modal.hidden = false;
    
    // Move focus to first element
    this.firstFocusable.focus();
    
    // Add event listeners
    this.modal.addEventListener('keydown', this.handleKeyDown.bind(this));
  }
  
  close() {
    // Hide modal
    this.modal.hidden = true;
    
    // Restore focus
    if (this.previousFocus) {
      this.previousFocus.focus();
    }
    
    // Remove listeners
    this.modal.removeEventListener('keydown', this.handleKeyDown);
  }
  
  handleKeyDown(e) {
    // Close on Escape
    if (e.key === 'Escape') {
      this.close();
      return;
    }
    
    // Focus trap on Tab
    if (e.key === 'Tab') {
      if (e.shiftKey) {
        // Shift + Tab
        if (document.activeElement === this.firstFocusable) {
          e.preventDefault();
          this.lastFocusable.focus();
        }
      } else {
        // Tab
        if (document.activeElement === this.lastFocusable) {
          e.preventDefault();
          this.firstFocusable.focus();
        }
      }
    }
  }
}

// Usage
const modal = new ModalDialog(document.querySelector('.modal'));
document.getElementById('open-modal-btn').addEventListener('click', () => {
  modal.open();
});
document.getElementById('cancel-btn').addEventListener('click', () => {
  modal.close();
});
</script>

Example: Roving tabindex for radio group

<div role="radiogroup" aria-labelledby="group-label">
  <p id="group-label">Choose your favorite color:</p>
  
  <div role="radio" aria-checked="true" tabindex="0">
    Red
  </div>
  <div role="radio" aria-checked="false" tabindex="-1">
    Green
  </div>
  <div role="radio" aria-checked="false" tabindex="-1">
    Blue
  </div>
</div>

<script>
const radioGroup = document.querySelector('[role="radiogroup"]');
const radios = Array.from(radioGroup.querySelectorAll('[role="radio"]'));

radioGroup.addEventListener('keydown', (e) => {
  const current = document.activeElement;
  const currentIndex = radios.indexOf(current);
  let nextIndex;
  
  switch(e.key) {
    case 'ArrowDown':
    case 'ArrowRight':
      e.preventDefault();
      nextIndex = (currentIndex + 1) % radios.length;
      break;
    case 'ArrowUp':
    case 'ArrowLeft':
      e.preventDefault();
      nextIndex = (currentIndex - 1 + radios.length) % radios.length;
      break;
    case ' ':
    case 'Enter':
      e.preventDefault();
      selectRadio(current);
      return;
    default:
      return;
  }
  
  // Update tabindex and focus
  radios[currentIndex].tabIndex = -1;
  radios[nextIndex].tabIndex = 0;
  radios[nextIndex].focus();
});

function selectRadio(radio) {
  radios.forEach(r => r.setAttribute('aria-checked', 'false'));
  radio.setAttribute('aria-checked', 'true');
}

// Click handler
radios.forEach(radio => {
  radio.addEventListener('click', () => selectRadio(radio));
});
</script>
Focus Best Practices: Always show visible focus indicator (:focus styles). Never use outline: none without alternative. Maintain logical tab order (matches visual flow). Use tabindex="0" for custom controls, never positive values. Trap focus in modals. Restore focus when closing overlays. Test with keyboard only (no mouse).

4. Alternative Text and Media Descriptions

Element Attribute Purpose Example
<img> alt Describes image content alt="Woman using laptop"
<img> (decorative) alt="" Empty for decorative images alt=""
<video> Text tracks Captions and descriptions <track kind="captions">
<audio> Fallback content Transcript link <a href="transcript.txt">
<figure> <figcaption> Extended description <figcaption>Chart showing...</figcaption>
SVG <title>, <desc> SVG accessibility <title>Icon name</title>
Icon fonts aria-label Describe icon purpose aria-label="Search"
Complex images aria-describedby Link to detailed description aria-describedby="chart-desc"

Example: Image Alt Text Guidelines

<!-- Informative image -->
<img src="graph.png" alt="Sales increased 25% in Q4 2023" />

<!-- Functional image (link/button) -->
<a href="home.html">
  <img src="logo.png" alt="Company Name Home" />
</a>

<!-- Decorative image -->
<img src="divider.png" alt="" role="presentation" />

<!-- Complex image with extended description -->
<figure>
  <img src="chart.png" alt="Revenue chart 2020-2023" 
       aria-describedby="chart-details" />
  <figcaption id="chart-details">
    Bar chart showing revenue growth from $2M in 2020 
    to $8M in 2023, with steady increase each year.
  </figcaption>
</figure>

<!-- Image in text flow -->
<p>
  Our CEO <img src="ceo.jpg" alt="Jane Smith" /> announced 
  the new initiative.
</p>

Example: Video with Captions and Audio Description

<video controls>
  <source src="video.mp4" type="video/mp4" />
  <source src="video.webm" type="video/webm" />
  
  <!-- Captions (subtitles) -->
  <track kind="captions" src="captions-en.vtt" 
         srclang="en" label="English" default />
  <track kind="captions" src="captions-es.vtt" 
         srclang="es" label="Spanish" />
  
  <!-- Audio descriptions for visually impaired -->
  <track kind="descriptions" src="descriptions.vtt" 
         srclang="en" label="Audio Description" />
  
  <!-- Fallback content -->
  <p>Your browser doesn't support video. 
     <a href="transcript.html">Read transcript</a>
  </p>
</video>

<!-- Provide transcript link -->
<p><a href="transcript.html">Full video transcript</a></p>

Example: Accessible SVG and Icon Fonts

<!-- SVG with title and description -->
<svg role="img" aria-labelledby="icon-title icon-desc">
  <title id="icon-title">Success</title>
  <desc id="icon-desc">Green checkmark indicating success</desc>
  <path d="M10 15 L5 10 L7 8 L10 11 L17 4 L19 6 Z" />
</svg>

<!-- Icon font with aria-label -->
<button>
  <i class="icon-trash" aria-hidden="true"></i>
  <span class="sr-only">Delete item</span>
</button>

<!-- Or using aria-label directly -->
<button aria-label="Close dialog">
  <span class="icon-close" aria-hidden="true">&times;</span>
</button>

<!-- Decorative SVG -->
<svg aria-hidden="true" focusable="false">
  <path d="..." />
</svg>
Alt Text Writing Guidelines: Be specific and concise (under 150 characters). Describe the information, not the image itself ("graph showing sales trends" not "graph image"). Include text visible in the image. Don't start with "image of" or "picture of". For functional images (links/buttons), describe the action/destination. Use alt="" for purely decorative images. Provide extended descriptions for complex images (charts, diagrams) using figcaption or aria-describedby.

5. Form Accessibility and Error Handling

Technique Implementation Purpose
Label association <label for="id"> Connect labels to inputs
Required fields required + aria-required="true" Indicate mandatory fields
Error messages aria-invalid + aria-describedby Associate errors with fields
Field instructions aria-describedby Provide helpful hints
Fieldset grouping <fieldset> + <legend> Group related inputs
Error summary role="alert" Announce validation errors
Input patterns pattern + title Define expected format
Autocomplete autocomplete attribute Help users fill forms faster

Example: Accessible Form with Validation

<form novalidate>
  <!-- Text input with label and hint -->
  <div class="form-group">
    <label for="username">
      Username <span class="required">*</span>
    </label>
    <input type="text" id="username" name="username" 
           required aria-required="true"
           aria-describedby="username-hint"
           autocomplete="username" />
    <small id="username-hint">
      Must be 3-20 characters, letters and numbers only
    </small>
  </div>

  <!-- Email with error message -->
  <div class="form-group">
    <label for="email">
      Email <span class="required">*</span>
    </label>
    <input type="email" id="email" name="email" 
           required aria-required="true"
           aria-invalid="true"
           aria-describedby="email-error"
           autocomplete="email" />
    <span id="email-error" class="error" role="alert">
      Please enter a valid email address
    </span>
  </div>

  <!-- Radio group -->
  <fieldset>
    <legend>Account type <span class="required">*</span></legend>
    <div>
      <input type="radio" id="personal" name="account" 
             value="personal" required />
      <label for="personal">Personal</label>
    </div>
    <div>
      <input type="radio" id="business" name="account" 
             value="business" />
      <label for="business">Business</label>
    </div>
  </fieldset>

  <!-- Checkbox -->
  <div class="form-group">
    <input type="checkbox" id="terms" name="terms" 
           required aria-required="true"
           aria-describedby="terms-error" />
    <label for="terms">
      I agree to the <a href="terms.html">terms</a>
    </label>
    <span id="terms-error" class="error" role="alert" 
          hidden>
      You must agree to the terms
    </span>
  </div>

  <button type="submit">Create Account</button>
</form>

Example: Error Summary and Focus Management

// Error summary at top of form
<div id="error-summary" class="error-summary" role="alert" 
     tabindex="-1" hidden>
  <h2>Please correct the following errors:</h2>
  <ul>
    <li><a href="#username">Username is required</a></li>
    <li><a href="#email">Email is invalid</a></li>
    <li><a href="#password">Password is too short</a></li>
  </ul>
</div>

<form id="signup-form">
  <!-- Form fields... -->
</form>

<script>
// Validation and error handling
const form = document.getElementById('signup-form');
const errorSummary = document.getElementById('error-summary');

form.addEventListener('submit', function(e) {
  e.preventDefault();
  
  const errors = validateForm();
  
  if (errors.length > 0) {
    // Show error summary
    errorSummary.hidden = false;
    
    // Populate error list
    const errorList = errorSummary.querySelector('ul');
    errorList.innerHTML = errors.map(error => 
      `<li><a href="#${error.field}">${error.message}</a></li>`
    ).join('');
    
    // Focus on error summary
    errorSummary.focus();
    
    // Mark invalid fields
    errors.forEach(error => {
      const field = document.getElementById(error.field);
      field.setAttribute('aria-invalid', 'true');
      
      // Show field-level error
      const errorMsg = document.getElementById(`${error.field}-error`);
      if (errorMsg) {
        errorMsg.hidden = false;
      }
    });
  } else {
    // Submit form
    this.submit();
  }
});

// Clear error on input
form.addEventListener('input', function(e) {
  if (e.target.matches('input, textarea, select')) {
    e.target.setAttribute('aria-invalid', 'false');
    const errorMsg = document.getElementById(`${e.target.id}-error`);
    if (errorMsg) {
      errorMsg.hidden = true;
    }
  }
});
</script>

Example: Autocomplete Attributes

<!-- Personal information -->
<input type="text" name="name" autocomplete="name" />
<input type="text" name="fname" autocomplete="given-name" />
<input type="text" name="lname" autocomplete="family-name" />
<input type="email" name="email" autocomplete="email" />
<input type="tel" name="phone" autocomplete="tel" />

<!-- Address fields -->
<input type="text" autocomplete="street-address" />
<input type="text" autocomplete="address-line1" />
<input type="text" autocomplete="address-line2" />
<input type="text" autocomplete="address-level2" /> <!-- City -->
<input type="text" autocomplete="address-level1" /> <!-- State -->
<input type="text" autocomplete="postal-code" />
<input type="text" autocomplete="country-name" />

<!-- Authentication -->
<input type="text" autocomplete="username" />
<input type="password" autocomplete="current-password" />
<input type="password" autocomplete="new-password" />

<!-- Payment -->
<input type="text" autocomplete="cc-name" />
<input type="text" autocomplete="cc-number" />
<input type="text" autocomplete="cc-exp" />
<input type="text" autocomplete="cc-csc" />
Form Accessibility Best Practices: Always use explicit <label> elements with for attribute. Mark required fields with required attribute AND visual indicator (*). Use aria-required="true" for custom controls. Provide clear, specific error messages associated with fields using aria-describedby. Mark invalid fields with aria-invalid="true". Show errors both at field level and in summary. Focus on error summary or first invalid field on submit. Use fieldset/legend for radio/checkbox groups. Provide instructions before the field, not just in placeholder. Use autocomplete attributes to help users. Test with screen reader and keyboard only.

6. Color Contrast and Visual Accessibility

Standard Contrast Ratio Requirement
WCAG 2.1 Level AA 4.5:1 Normal text (under 18pt or 14pt bold)
WCAG 2.1 Level AA 3:1 Large text (18pt+ or 14pt+ bold)
WCAG 2.1 Level AAA 7:1 Normal text (enhanced)
WCAG 2.1 Level AAA 4.5:1 Large text (enhanced)
UI Components 3:1 Graphical objects, form controls
Focus indicators 3:1 Against adjacent colors
Technique Purpose Implementation
Color + pattern Don't rely on color alone Use icons, shapes, patterns, labels
Visible focus Show keyboard focus :focus and :focus-visible styles
Link identification Distinguish from text Underline, bold, or 3:1 contrast + indicator
Text over images Ensure readability Overlay, shadows, sufficient contrast
Resize text Support 200% zoom Relative units (rem, em), no text in images
Reflow 320px width support Responsive design, avoid horizontal scroll
Animation control Reduce motion prefers-reduced-motion media query

Example: Color Contrast Examples

<!-- Good contrast (AA compliant) -->
<p style="color: #333; background: #fff;">
  Dark gray on white (11:1 ratio) ✓
</p>

<button style="color: #fff; background: #0066cc;">
  White on blue (6.5:1 ratio) ✓
</button>

<!-- Insufficient contrast (fails AA) -->
<p style="color: #999; background: #fff;">
  Light gray on white (2.8:1 ratio) ✗
</p>

<a href="#" style="color: #87ceeb; background: #fff;">
  Sky blue on white (2.4:1 ratio) ✗
</a>

<!-- Large text can use lower contrast -->
<h1 style="font-size: 24pt; color: #777; background: #fff;">
  Large heading (3.4:1 ratio) ✓ (AA for large text)
</h1>

Example: Not Relying on Color Alone

<!-- Bad: Color only -->
<p>
  Required fields are in <span style="color: red;">red</span>.
</p>

<!-- Good: Color + symbol -->
<label for="name">
  Name <span class="required" aria-label="required">*</span>
</label>

<!-- Bad: Color-coded status -->
<div class="status-green">Active</div>
<div class="status-red">Inactive</div>

<!-- Good: Color + icon + text -->
<div class="status status-active">
  <svg aria-hidden="true"><!-- checkmark icon --></svg>
  <span>Active</span>
</div>
<div class="status status-inactive">
  <svg aria-hidden="true"><!-- x icon --></svg>
  <span>Inactive</span>
</div>

<!-- Charts: use patterns in addition to colors -->
<svg role="img" aria-label="Sales by region">
  <!-- Use different fill patterns for each segment -->
  <rect fill="url(#pattern-stripes)" />
  <rect fill="url(#pattern-dots)" />
  <defs>
    <pattern id="pattern-stripes">...</pattern>
    <pattern id="pattern-dots">...</pattern>
  </defs>
</svg>

Example: Focus Indicators and Reduced Motion

<style>
/* Visible focus indicator */
a:focus,
button:focus,
input:focus {
  outline: 3px solid #0066cc;
  outline-offset: 2px;
}

/* Enhanced focus for keyboard navigation */
:focus-visible {
  outline: 3px solid #0066cc;
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(0, 102, 204, 0.25);
}

/* Remove outline for mouse clicks (but keep for keyboard) */
:focus:not(:focus-visible) {
  outline: none;
}

/* High contrast links */
a {
  color: #0066cc;
  text-decoration: underline;
  text-underline-offset: 2px;
}

a:hover {
  color: #004499;
  text-decoration-thickness: 2px;
}

/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* Default smooth animations */
@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 0.3s ease, box-shadow 0.3s ease;
  }
  
  .card:hover {
    transform: translateY(-4px);
    box-shadow: 0 8px 16px rgba(0,0,0,0.2);
  }
}

/* Text resize support */
html {
  font-size: 16px; /* Base size */
}

body {
  font-size: 1rem; /* Relative to root */
  line-height: 1.5;
}

h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
p { font-size: 1rem; }

/* Ensure text can scale to 200% */
.container {
  max-width: 1200px;
  padding: 0 1rem;
}

@media (max-width: 480px) {
  /* Reflow content, no horizontal scroll */
  .grid {
    grid-template-columns: 1fr;
  }
}
</style>

Example: Text Over Images

<!-- Technique 1: Dark overlay -->
<div class="hero" style="
  background-image: url('photo.jpg');
  background-size: cover;
  position: relative;
">
  <div style="
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
  "></div>
  <h1 style="
    position: relative;
    color: white;
    z-index: 1;
  ">
    Hero Title (Sufficient Contrast)
  </h1>
</div>

<!-- Technique 2: Text shadow -->
<div class="banner" style="background-image: url('bg.jpg');">
  <h2 style="
    color: white;
    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8),
                 0 0 8px rgba(0, 0, 0, 0.6);
  ">
    Heading with Shadow
  </h2>
</div>

<!-- Technique 3: Background box -->
<div class="card" style="background-image: url('pattern.jpg');">
  <p style="
    background: rgba(255, 255, 255, 0.95);
    padding: 1rem;
    color: #333;
  ">
    Text in semi-transparent box
  </p>
</div>
Visual Accessibility Checklist: Ensure 4.5:1 contrast for normal text, 3:1 for large text and UI components. Test with contrast checker tools. Never use color alone to convey information—add icons, patterns, or text labels. Provide visible focus indicators (minimum 3:1 contrast). Make links distinguishable from regular text (underline or sufficient contrast difference). Support text resize up to 200% without loss of content or functionality. Ensure content reflows at 320px width without horizontal scrolling. Respect prefers-reduced-motion for animations. Avoid text in images; use real text with CSS styling. Test with different zoom levels, color blindness simulators, and high contrast mode.

Section 14: Key Takeaways

  • Semantic HTML: Use proper semantic elements (header, nav, main, article, aside, footer) to create document structure that screen readers can navigate effectively
  • ARIA: Enhance accessibility with ARIA roles, states, and properties when native HTML semantics are insufficient; use aria-label, aria-labelledby, aria-describedby, aria-live
  • Keyboard Navigation: Ensure all interactive elements are keyboard accessible with Tab, Enter, Space, Arrow keys; manage focus properly with tabindex, skip links, and focus traps
  • Alternative Text: Provide meaningful alt text for images (descriptive, concise, under 150 chars); use alt="" for decorative images; provide captions and transcripts for multimedia
  • Form Accessibility: Associate labels with inputs using for attribute; mark required fields; show inline validation errors with aria-invalid and aria-describedby; use autocomplete attributes
  • Color Contrast: Maintain minimum 4.5:1 ratio for normal text, 3:1 for large text and UI components; never rely on color alone—add icons, patterns, or text labels
  • Focus Indicators: Always provide visible focus styles (:focus, :focus-visible) with minimum 3:1 contrast; never remove outlines without accessible alternatives
  • Motion Sensitivity: Respect prefers-reduced-motion media query to disable or reduce animations for users with vestibular disorders
  • Text Scaling: Use relative units (rem, em) to support text resize up to 200%; ensure content reflows at 320px width without horizontal scrolling
  • Testing: Test with keyboard only (no mouse), screen readers (NVDA, JAWS, VoiceOver), color contrast checkers, and browser accessibility tools to verify compliance with WCAG 2.1 Level AA