Back to Homepage

International Standards

Back to Homepage
Best Practices

Web Accessibility Best Practices

A comprehensive guide to building accessible websites that work for everyone - covering HTML, CSS, JavaScript, ARIA, and modern web development techniques for WCAG 2.1 Level AA compliance

Accessibility is Not Optional
Web accessibility is a legal requirement in many jurisdictions (EU, Germany, US, UK, Canada, Australia) and a moral imperative. More importantly, it's good design and development practice that benefits all users. This guide provides actionable best practices for creating accessible web experiences that comply with WCAG 2.1 Level AA and work beautifully for everyone.

Introduction to Web Accessibility

Why Web Accessibility Matters

Web accessibility ensures that websites, tools, and technologies are designed and developed so that people with disabilities can use them. More specifically, people can perceive, understand, navigate, interact with, and contribute to the web.

Accessibility encompasses all disabilities that affect access to the web:

  • Visual: Blindness, low vision, color blindness
  • Auditory: Deafness, hard of hearing
  • Motor: Limited fine motor control, inability to use a mouse
  • Cognitive: Learning disabilities, distractibility, memory impairments
  • Neurological: Seizure disorders, attention deficit
  • Speech: Difficulty producing speech that is recognizable by voice interfaces

Beyond disabilities, accessible websites benefit everyone: older adults with changing abilities, mobile users in bright sunlight, people with temporary impairments (broken arm, eye surgery), users on slow connections, and people in noisy or quiet environments where they can't use audio.

WCAG 2.1 Level AA: The Standard

The Web Content Accessibility Guidelines (WCAG) 2.1 Level AA is the internationally recognized standard for web accessibility. It's referenced by laws worldwide including the ADA (US), EAA (EU), BFSG (Germany), and many others.

WCAG 2.1 is organized around four principles known as POUR:

  • Perceivable: Information and user interface components must be presentable to users in ways they can perceive
  • Operable: User interface components and navigation must be operable
  • Understandable: Information and the operation of user interface must be understandable
  • Robust: Content must be robust enough that it can be interpreted by a wide variety of user agents, including assistive technologies

WCAG has three conformance levels: A (lowest), AA (mid-range, legally required in most jurisdictions), and AAA (highest). This guide focuses on Level AA compliance.

Approach to Accessibility

Effective accessibility requires thinking about it from the beginning, not bolting it on at the end. Follow these principles:

  • Shift left: Consider accessibility during design and requirements gathering, not just testing
  • Semantic first: Use proper HTML semantics before reaching for ARIA
  • Progressive enhancement: Build a solid accessible foundation, then enhance
  • Test with real users: Automated tools catch only 30-40% of issues; test with assistive technologies and real users
  • Make it a habit: Integrate accessibility into your workflow so it becomes automatic

Semantic HTML: The Foundation

Use Semantic HTML Elements

Semantic HTML is the single most important aspect of web accessibility. Screen readers, keyboard navigation, and browser accessibility features all rely on proper HTML structure.

Landmark Elements

Use HTML5 landmark elements to define page regions. Screen reader users can jump between landmarks for quick navigation.

<!-- GOOD: Semantic landmarks -->
<header>
  <nav>
    <!-- Main navigation -->
  </nav>
</header>

<main>
  <article>
    <!-- Primary content -->
  </article>

  <aside>
    <!-- Sidebar/supplementary content -->
  </aside>
</main>

<footer>
  <!-- Footer content -->
</footer>

<!-- BAD: Generic divs -->
<div class="header">
  <div class="nav"></div>
</div>
<div class="content"></div>
<div class="footer"></div>
Landmark Rules
Use exactly one <main> per page. You can have multiple <nav>, <aside>, and <section> elements, but label them with aria-label if there are multiple of the same type (e.g., <nav aria-label="Primary"> and <nav aria-label="Footer">).

Heading Hierarchy

Headings (<h1>-<h6>) create a document outline. Screen reader users navigate by headings, so proper hierarchy is crucial.

<!-- GOOD: Logical heading hierarchy -->
<h1>Page Title</h1>
  <h2>Section 1</h2>
    <h3>Subsection 1.1</h3>
    <h3>Subsection 1.2</h3>
  <h2>Section 2</h2>
    <h3>Subsection 2.1</h3>

<!-- BAD: Skipped levels and styling-based choices -->
<h1>Page Title</h1>
  <h4>Section (h4 because it looks better)</h4>
  <h2>Another Section</h2>

Best practices:

  • One <h1> per page (typically the page title)
  • Don't skip heading levels (h1 → h3 without h2 is wrong)
  • Use CSS to style headings, not heading levels to achieve visual design
  • Headings should be descriptive and unique where possible

Lists

Use proper list markup for related items. Screen readers announce "List with X items" and allow users to skip through lists.

<!-- GOOD: Semantic lists -->
<ul>
  <li>Unordered item 1</li>
  <li>Unordered item 2</li>
</ul>

<ol>
  <li>Step 1</li>
  <li>Step 2</li>
</ol>

<dl>
  <dt>Term</dt>
  <dd>Definition</dd>
</dl>

<!-- BAD: Fake lists with divs -->
<div class="list">
  <div class="list-item">• Item 1</div>
  <div class="list-item">• Item 2</div>
</div>

Accessible Forms

Forms are critical for user interaction and must be fully accessible. Proper form markup ensures screen reader users understand what to enter and receive feedback about errors.

Always Use Labels

Every form input must have an associated <label>. Labels make the clickable area larger and provide essential information to screen readers.

<!-- GOOD: Proper label association -->
<label for="email">Email Address</label>
<input type="email" id="email" name="email" required>

<!-- ALSO GOOD: Implicit association -->
<label>
  Password
  <input type="password" name="password" required>
</label>

<!-- BAD: No label -->
<input type="text" placeholder="Enter name">

<!-- BAD: Placeholder is not a label -->
<input type="email" placeholder="Email">

Provide Instructions and Hints

Use aria-describedby to associate instructions with inputs.

<label for="password">Password</label>
<input
  type="password"
  id="password"
  aria-describedby="password-hint"
  required>
<div id="password-hint">
  Must be at least 8 characters with 1 number and 1 special character
</div>

Error Handling

Provide clear, accessible error messages associated with the specific fields that have errors.

<label for="email">Email Address</label>
<input
  type="email"
  id="email"
  aria-invalid="true"
  aria-describedby="email-error">
<div id="email-error" role="alert">
  Please enter a valid email address
</div>

Error message best practices:

  • Use aria-invalid="true" on fields with errors
  • Use role="alert" so screen readers announce errors immediately
  • Provide specific, actionable error messages (not just "Error")
  • Use color AND text/icons to indicate errors (not color alone)
  • Move focus to the first error or error summary after submission

Required Fields

Mark required fields programmatically and visually.

<label for="name">
  Full Name <span aria-label="required">*</span>
</label>
<input type="text" id="name" required aria-required="true">

<!-- OR include "required" in the label text -->
<label for="email">Email Address (required)</label>
<input type="email" id="email" required>

Form Groups: Fieldset and Legend

Use <fieldset> and <legend> to group related form elements, especially radio buttons and checkboxes.

<fieldset>
  <legend>Choose your subscription plan</legend>
  <label>
    <input type="radio" name="plan" value="free">
    Free
  </label>
  <label>
    <input type="radio" name="plan" value="pro">
    Pro
  </label>
</fieldset>

Accessible Tables

Use tables for tabular data, never for layout. Proper table markup helps screen reader users understand relationships between data.

<table>
  <caption>Sales Report Q1 2025</caption>
  <thead>
    <tr>
      <th scope="col">Month</th>
      <th scope="col">Revenue</th>
      <th scope="col">Profit</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">January</th>
      <td>$50,000</td>
      <td>$12,000</td>
    </tr>
  </tbody>
</table>

Table best practices:

  • Always include <caption> or aria-label to describe the table
  • Use <th> for headers with scope="col" or scope="row"
  • Use <thead>, <tbody>, and <tfoot> to group rows
  • For complex tables, use headers attribute to associate data cells with multiple headers
  • Never use tables for layout purposes

Images and Multimedia

Alternative Text for Images

Every <img> must have an alt attribute. Alt text should convey the purpose and content of the image, not just describe it.

Informative Images

For images that convey information, describe the content and meaning.

<!-- GOOD: Descriptive alt text -->
<img src="chart.png" alt="Bar chart showing 50% increase in sales from 2024 to 2025">

<!-- BAD: Not descriptive enough -->
<img src="chart.png" alt="Chart">
<img src="chart.png" alt="Bar chart">

Decorative Images

For purely decorative images that don't add information, use empty alt text to hide them from screen readers.

<!-- GOOD: Empty alt for decorative image -->
<img src="divider.png" alt="">

<!-- ALSO GOOD: CSS background image for decorative content -->
<div style="background-image: url(decoration.png)"></div>

Functional Images (Links and Buttons)

For images inside links or buttons, describe the action, not the image.

<!-- GOOD: Describes action -->
<a href="/search">
  <img src="search-icon.png" alt="Search">
</a>

<button>
  <img src="close.png" alt="Close dialog">
</button>

<!-- BAD: Describes image instead of action -->
<a href="/search">
  <img src="search-icon.png" alt="Magnifying glass icon">
</a>

Complex Images

For complex images like infographics, charts, or diagrams, provide both short alt text and a longer description.

<img
  src="sales-data.png"
  alt="Quarterly sales performance graph"
  aria-describedby="graph-description">

<div id="graph-description">
  <h3>Detailed Description</h3>
  <p>The graph shows quarterly sales from Q1 2024 to Q4 2024...</p>
  <ul>
    <li>Q1: $120,000</li>
    <li>Q2: $145,000</li>
    ...
  </ul>
</div>
Writing Effective Alt Text
DO: Be specific and concise; convey the purpose; describe content and function; keep it under 150 characters when possible.

DON'T: Start with "image of" or "picture of"; use redundant phrases; write essays; forget to consider context.

Video and Audio Content

Captions for Video

All pre-recorded video with audio must have synchronized captions. Captions benefit deaf users, people in sound-sensitive environments, non-native speakers, and users with audio processing difficulties.

<video controls>
  <source src="video.mp4" type="video/mp4">
  <track kind="captions" src="captions-en.vtt" srclang="en" label="English" default>
  <track kind="captions" src="captions-de.vtt" srclang="de" label="Deutsch">
</video>

Caption best practices:

  • Include all spoken words and meaningful sounds (applause, music, sound effects)
  • Identify speakers when not obvious
  • Use proper grammar, spelling, and punctuation
  • Synchronize captions with audio timing
  • Keep caption length readable (1-2 lines at a time)

Audio Descriptions

Video content should have audio descriptions narrating important visual information during pauses in dialogue.

<video controls>
  <source src="video.mp4" type="video/mp4">
  <track kind="descriptions" src="descriptions.vtt" srclang="en" label="Audio Descriptions">
</video>

Transcripts

Provide text transcripts for audio-only content (podcasts, audio interviews) and as an additional alternative for video.

<audio controls>
  <source src="podcast.mp3" type="audio/mpeg">
  Your browser does not support the audio element.
</audio>
<p><a href="transcript.html">Read transcript</a></p>

Accessible Media Controls

Ensure media players have accessible controls:

  • Use the browser's native controls (controls attribute) when possible
  • If using custom controls, ensure all buttons are keyboard accessible and properly labeled
  • Provide play/pause, volume control, seeking, and caption toggle
  • Never auto-play video with sound

Icons and SVG

Decorative Icons

If an icon is purely decorative (adjacent text already describes the action), hide it from screen readers.

<button>
  <svg aria-hidden="true" focusable="false">
    <!-- icon markup -->
  </svg>
  Save
</button>

Meaningful Icons (No Text)

If an icon stands alone without text, provide accessible text.

<button aria-label="Close">
  <svg aria-hidden="true" focusable="false">
    <!-- X icon -->
  </svg>
</button>

<!-- OR use visually hidden text -->
<button>
  <svg aria-hidden="true" focusable="false">
    <!-- icon -->
  </svg>
  <span class="sr-only">Close</span>
</button>

Accessible SVG Graphics

For informative SVG images, use <title> and <desc> elements.

<svg role="img" aria-labelledby="chart-title chart-desc">
  <title id="chart-title">Sales Growth Chart</title>
  <desc id="chart-desc">Bar chart showing 25% sales growth from 2024 to 2025</desc>
  <!-- SVG content -->
</svg>

Keyboard Accessibility

Keyboard Navigation Fundamentals

All functionality must be available via keyboard. Many users cannot use a mouse due to motor disabilities, and power users often prefer keyboard navigation.

Standard keyboard interactions:

  • Tab: Move focus forward through interactive elements
  • Shift + Tab: Move focus backward
  • Enter: Activate links and buttons
  • Space: Activate buttons, toggle checkboxes, select radio buttons
  • Arrow keys: Navigate within components (radio groups, select dropdowns, tabs)
  • Escape: Close dialogs, cancel operations
  • Home/End: Jump to first/last item in lists or inputs

Focus Management

Visible Focus Indicator

Keyboard users must always know where focus is. Never remove focus outlines without providing an alternative indicator.

/* BAD: Removing focus outline entirely */
button:focus {
  outline: none;
}

/* GOOD: Custom focus style */
button:focus {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

/* BETTER: Use :focus-visible for keyboard-only focus */
button:focus {
  outline: none; /* Remove for mouse clicks */
}

button:focus-visible {
  outline: 2px solid #2563eb; /* Show for keyboard */
  outline-offset: 2px;
}

Logical Focus Order

Tab order should follow visual order and logical flow. Avoid using tabindex values greater than 0.

<!-- GOOD: Natural DOM order determines tab order -->
<button>First</button>
<button>Second</button>
<button>Third</button>

<!-- BAD: Positive tabindex disrupts natural order -->
<button tabindex="3">Third</button>
<button tabindex="1">First</button>
<button tabindex="2">Second</button>

<!-- ACCEPTABLE: tabindex="-1" for programmatic focus -->
<div tabindex="-1" id="error-summary">
  Please fix the following errors...
</div>

Focus Trapping in Modals

When a modal dialog opens, focus should move into it and remain trapped until closed.

// Focus trap implementation
function trapFocus(element) {
  const focusableElements = element.querySelectorAll(
    'a[href], button:not([disabled]), textarea, input, select'
  );
  const firstFocusable = focusableElements[0];
  const lastFocusable = focusableElements[focusableElements.length - 1];

  element.addEventListener('keydown', (e) => {
    if (e.key === 'Tab') {
      if (e.shiftKey) {
        if (document.activeElement === firstFocusable) {
          e.preventDefault();
          lastFocusable.focus();
        }
      } else {
        if (document.activeElement === lastFocusable) {
          e.preventDefault();
          firstFocusable.focus();
        }
      }
    }
  });

  firstFocusable.focus();
}

ARIA: When and How to Use It

The Five Rules of ARIA

ARIA (Accessible Rich Internet Applications) adds semantic meaning to HTML elements. However, it should be used carefully:

  1. First Rule: Don't use ARIA if you can use a native HTML element or attribute with the semantics and behavior you require already built in
  2. Second Rule: Do not change native semantics unless you really have to
  3. Third Rule: All interactive ARIA controls must be keyboard accessible
  4. Fourth Rule: Do not use role="presentation" or aria-hidden="true" on focusable elements
  5. Fifth Rule: All interactive elements must have an accessible name
Use Semantic HTML First
ARIA is a last resort, not a first choice. A native <button> is always better than <div role="button">. ARIA doesn't add behavior—it only changes how assistive technologies interpret elements. If you find yourself using extensive ARIA, reconsider your HTML structure.

Common ARIA Attributes

aria-label and aria-labelledby

Use these to provide accessible names when visual labels aren't sufficient.

<!-- aria-label: Direct label -->
<button aria-label="Close dialog">
  <svg>...</svg>
</button>

<!-- aria-labelledby: Reference another element -->
<div role="dialog" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm Delete</h2>
  ...
</div>

<!-- Multiple labels -->
<button aria-labelledby="btn-label icon-label">
  <span id="btn-label">Download</span>
  <svg aria-hidden="true">...</svg>
  <span id="icon-label" class="sr-only">PDF file</span>
</button>

aria-describedby

Provides additional description or instructions.

<input
  type="text"
  id="username"
  aria-describedby="username-hint username-error">
<div id="username-hint">Must be 3-20 characters</div>
<div id="username-error" hidden>Username already taken</div>

aria-hidden

Hides content from screen readers (but not visually). Use sparingly.

<!-- Hide decorative icons -->
<button>
  <svg aria-hidden="true">...</svg>
  Save
</button>

<!-- Hide duplicate content -->
<div aria-hidden="true">★★★★☆</div>
<span class="sr-only">4 out of 5 stars</span>

aria-live Regions

Announce dynamic content changes to screen readers.

<!-- Polite: Announce when screen reader is idle -->
<div aria-live="polite" aria-atomic="true">
  Items in cart: 3
</div>

<!-- Assertive: Announce immediately (use sparingly) -->
<div role="alert" aria-live="assertive">
  Error: Payment failed
</div>

<!-- Status: For status updates -->
<div role="status" aria-live="polite">
  Loading...
</div>

Common ARIA Design Patterns

Accordion

<div class="accordion">
  <h3>
    <button
      aria-expanded="false"
      aria-controls="panel1"
      id="accordion1">
      Section 1
    </button>
  </h3>
  <div
    id="panel1"
    role="region"
    aria-labelledby="accordion1"
    hidden>
    Content...
  </div>
</div>

Tabs

<div class="tabs">
  <div role="tablist" aria-label="Products">
    <button
      role="tab"
      aria-selected="true"
      aria-controls="panel-1"
      id="tab-1">
      Description
    </button>
    <button
      role="tab"
      aria-selected="false"
      aria-controls="panel-2"
      id="tab-2"
      tabindex="-1">
      Reviews
    </button>
  </div>

  <div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
    Description content...
  </div>
  <div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
    Reviews content...
  </div>
</div>

Modal Dialog

<div
  role="dialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc"
  aria-modal="true">
  <h2 id="dialog-title">Confirm Action</h2>
  <p id="dialog-desc">Are you sure you want to delete this item?</p>
  <button>Cancel</button>
  <button>Delete</button>
</div>
ARIA Authoring Practices Guide
The W3C ARIA Authoring Practices Guide (APG) provides detailed patterns and code examples for common widgets. Reference it when building custom components: w3.org/WAI/ARIA/apg

Color and Visual Design

Color Contrast Requirements

Sufficient color contrast ensures text is readable for users with low vision or color blindness.

WCAG 2.1 Level AA contrast requirements:

  • Normal text: 4.5:1 minimum contrast ratio against background
  • Large text: 3:1 minimum (18pt regular or 14pt bold and larger)
  • UI components and graphics: 3:1 minimum for borders, icons, and interactive elements
/* GOOD: Sufficient contrast */
.text {
  color: #333333; /* Dark gray */
  background: #FFFFFF; /* White */
  /* Contrast ratio: 12.6:1 ✓ */
}

.button {
  color: #FFFFFF; /* White */
  background: #0066CC; /* Blue */
  /* Contrast ratio: 6.6:1 ✓ */
}

/* BAD: Insufficient contrast */
.subtle-text {
  color: #AAAAAA; /* Light gray */
  background: #FFFFFF; /* White */
  /* Contrast ratio: 2.3:1 ✗ FAILS */
}

.link {
  color: #90CAF9; /* Light blue */
  background: #FFFFFF; /* White */
  /* Contrast ratio: 2.0:1 ✗ FAILS */
}
Contrast Checking Tools
Use contrast checkers during design:
  • WebAIM Contrast Checker: webaim.org/resources/contrastchecker
  • Contrast Ratio: contrast-ratio.com
  • Browser DevTools: Chrome and Firefox have built-in contrast checkers

Don't Rely on Color Alone

Color should not be the only visual means of conveying information. Add text, patterns, icons, or other indicators.

<!-- BAD: Color only -->
<span style="color: red;">Error</span>
<span style="color: green;">Success</span>

<!-- GOOD: Color + text/icon -->
<span class="error">
  <svg aria-hidden="true">⚠</svg>
  Error: Invalid input
</span>
<span class="success">
  <svg aria-hidden="true">✓</svg>
  Success: Saved
</span>

<!-- Graph example -->
<!-- BAD: Color-coded lines only -->
<!-- GOOD: Color + patterns/labels -->
<svg>
  <line stroke="blue" stroke-dasharray="5,5" /> <!-- Dashed -->
  <text>Category A</text>
  <line stroke="red" stroke-dasharray="10,5" /> <!-- Different dash -->
  <text>Category B</text>
</svg>

Text Sizing and Zoom

Content must be readable and functional when text is resized up to 200% without assistive technology.

  • Use relative units (rem, em, %) instead of fixed pixels for font sizes
  • Don't disable browser zoom with <meta name="viewport" maximum-scale=1>
  • Ensure content reflows at 320px width (mobile devices zoomed to 400%)
  • Allow user to adjust text spacing without breaking layout
/* GOOD: Relative units */
body {
  font-size: 100%; /* 16px typically */
}

h1 {
  font-size: 2.5rem; /* 40px */
}

p {
  font-size: 1rem; /* 16px */
  line-height: 1.5; /* 24px */
}

/* BAD: Fixed pixels */
body {
  font-size: 16px;
}

h1 {
  font-size: 40px;
}

Responsive and Mobile Accessibility

  • Touch targets: Minimum 44×44 CSS pixels for buttons and links (48×48 preferred)
  • Spacing: Adequate spacing between interactive elements to prevent accidental activation
  • Orientation: Support both portrait and landscape unless specific orientation is essential
  • Motion: Provide controls to pause, stop, or hide moving content; respect prefers-reduced-motion
/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
  * {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* Touch target sizing */
button,
a {
  min-width: 44px;
  min-height: 44px;
  padding: 12px;
}

Testing for Accessibility

Comprehensive Testing Strategy

Accessibility testing requires multiple approaches. No single tool catches all issues.

  1. Automated testing: Catches ~30-40% of issues quickly
  2. Keyboard testing: Manual testing with keyboard only
  3. Screen reader testing: Testing with NVDA, JAWS, VoiceOver, or TalkBack
  4. Manual inspection: Checking code, color contrast, text alternatives
  5. User testing: Testing with real users with disabilities (gold standard)

Automated Testing Tools

  • Our Accessibility Checker: Integrated tool for quick scans
  • axe DevTools: Browser extension for Chrome/Firefox, highly accurate
  • WAVE: Visual tool showing issues directly on page
  • Lighthouse: Built into Chrome DevTools, includes accessibility audit
  • Pa11y: Command-line tool for CI/CD integration
  • axe-core: JavaScript library for custom testing automation

Keyboard Testing Checklist

Unplug your mouse and test your site with keyboard only:

  • Can you reach all interactive elements with Tab?
  • Is focus visible at all times?
  • Does Tab order follow visual order?
  • Can you activate all buttons and links?
  • Can you close modals with Escape?
  • Do custom widgets work with arrow keys?
  • Can you submit forms with Enter?
  • Are there keyboard traps (places you can't escape from)?

Screen Reader Testing Basics

Common screen readers:

  • NVDA (Windows): Free, widely used, excellent for testing
  • JAWS (Windows): Most popular commercial screen reader
  • VoiceOver (Mac/iOS): Built into Apple devices
  • TalkBack (Android): Built into Android
  • ORCA (Linux): Open source screen reader for Linux

What to test:

  • Can you navigate by headings?
  • Are landmarks announced?
  • Do form fields have labels?
  • Are error messages announced?
  • Do images have appropriate alt text?
  • Are dynamic content changes announced?
  • Do custom widgets announce their role and state?
Make Accessibility Part of Your Workflow
The key to maintainable accessibility is integrating it into your development process:
  • Run automated tests in CI/CD pipelines
  • Include accessibility in code reviews
  • Test with keyboard during development, not just at the end
  • Keep accessibility in mind during design and requirements phases
  • Educate your entire team on accessibility fundamentals