Web Accessibility Best Practices

Build websites that everyone can use! Learn accessibility best practices including WCAG guidelines, ARIA attributes, semantic HTML, keyboard navigation, and testing techniques to create inclusive web experiences.

Why Accessibility Matters

Over 1 billion people worldwide have disabilities. Accessible websites benefit everyone:

Accessibility (often abbreviated as “a11y”) isn’t optional—it’s essential.

WCAG Principles: POUR

Web Content Accessibility Guidelines (WCAG) are built on four principles:

  1. Perceivable — Information must be presentable to users
  2. Operable — Interface must be usable
  3. Understandable — Content must be comprehensible
  4. Robust — Content must work with assistive technologies

Let’s apply each principle!

Semantic HTML Foundation

Use semantic elements for better accessibility:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Page Structure</title>
</head>
<body>
<!-- Recommended approach: Semantic HTML structure -->
<header>
  <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>
</header>

<main>
  <article>
    <h1>Article Title</h1>
    
    <section>
      <h2>Introduction</h2>
      <p>Article introduction...</p>
    </section>
    
    <section>
      <h2>Main Content</h2>
      <p>Main article content...</p>
    </section>
  </article>
  
  <aside>
    <h2>Related Articles</h2>
    <ul>
      <li><a href="/related1">Related Article 1</a></li>
      <li><a href="/related2">Related Article 2</a></li>
    </ul>
  </aside>
</main>

<footer>
  <p>&copy; 2025 Company Name</p>
</footer>

<!-- Problematic example: Non-semantic structure -->
<!--
<div class="header">
  <div class="nav">...</div>
</div>
<div class="content">...</div>
<div class="footer">...</div>
-->
</body>
</html>

Why semantic HTML matters:

Alt Text for Images

Every image needs descriptive alternative text:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Alt Text Examples</title>
</head>
<body>
<!-- Recommended approach: Descriptive alt text -->
<img 
  src="golden-retriever-playing-fetch.jpg"
  alt="Golden retriever running through grass with tennis ball in mouth"
  width="600"
  height="400">

<!-- Recommended approach: Informative alt for charts -->
<img 
  src="sales-chart-2024.png"
  alt="Bar chart showing 25% increase in Q4 2024 sales compared to Q3"
  width="800"
  height="600">

<!-- Recommended approach: Empty alt for decorative images -->
<img 
  src="decorative-border.svg"
  alt=""
  role="presentation">

<!-- Recommended approach: Alt for functional images -->
<a href="/search">
  <img 
    src="search-icon.svg"
    alt="Search"
    width="24"
    height="24">
</a>

<!-- Problematic example: Missing alt -->
<!--
<img src="important-diagram.jpg">
-->

<!-- Problematic example: Meaningless alt -->
<!--
<img src="photo.jpg" alt="image">
<img src="pic1.jpg" alt="pic1">
-->

<!-- Problematic example: Redundant text -->
<!--
<img src="dog.jpg" alt="Image of a dog">
<img src="chart.jpg" alt="Photo of a chart">
-->

<!-- Complex image with detailed description -->
<figure>
  <img 
    src="complex-infographic.jpg"
    alt="Infographic comparing renewable energy adoption across countries"
    aria-describedby="infographic-desc">
  <figcaption id="infographic-desc">
    Detailed data visualization showing solar, wind, and hydro power
    adoption rates in USA (35%), Germany (42%), and China (28%)
    over the last decade.
  </figcaption>
</figure>
</body>
</html>

Alt text guidelines:

ARIA Labels and Roles

ARIA (Accessible Rich Internet Applications) enhances HTML accessibility:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ARIA Attributes</title>
</head>
<body>
<!-- aria-label for context -->
<nav aria-label="Main navigation">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/products">Products</a></li>
  </ul>
</nav>

<nav aria-label="Footer navigation">
  <ul>
    <li><a href="/privacy">Privacy</a></li>
    <li><a href="/terms">Terms</a></li>
  </ul>
</nav>

<!-- aria-labelledby (reference to visible text) -->
<section aria-labelledby="features-heading">
  <h2 id="features-heading">Key Features</h2>
  <p>Our product offers...</p>
</section>

<!-- aria-describedby (additional description) -->
<button 
  aria-label="Delete item"
  aria-describedby="delete-warning">
  🗑️
</button>
<span id="delete-warning" class="sr-only">
  This action cannot be undone
</span>

<!-- aria-expanded for collapsible content -->
<button 
  aria-expanded="false"
  aria-controls="dropdown-menu"
  onclick="toggleDropdown(this)">
  Menu
</button>
<ul id="dropdown-menu" hidden>
  <li><a href="/option1">Option 1</a></li>
  <li><a href="/option2">Option 2</a></li>
</ul>

<!-- aria-live for dynamic content -->
<div 
  role="status"
  aria-live="polite"
  aria-atomic="true"
  id="status-message">
  <!-- Dynamic messages appear here -->
</div>

<!-- aria-current for navigation -->
<nav>
  <a href="/" aria-current="page">Home</a>
  <a href="/about">About</a>
  <a href="/contact">Contact</a>
</nav>

<!-- Custom widget with role -->
<div role="tablist">
  <button 
    role="tab"
    aria-selected="true"
    aria-controls="panel1"
    id="tab1">
    Tab 1
  </button>
  <button 
    role="tab"
    aria-selected="false"
    aria-controls="panel2"
    id="tab2">
    Tab 2
  </button>
</div>

<div 
  role="tabpanel"
  id="panel1"
  aria-labelledby="tab1">
  Content for Tab 1
</div>

<script>
  function toggleDropdown(button) {
    const menu = document.getElementById('dropdown-menu');
    const isExpanded = button.getAttribute('aria-expanded') === 'true';
    
    button.setAttribute('aria-expanded', !isExpanded);
    menu.hidden = isExpanded;
  }
</script>

<style>
  /* Screen reader only class */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
  }
</style>
</body>
</html>

Key ARIA attributes:

Keyboard Navigation

Ensure all functionality works with keyboard:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Keyboard Accessibility</title>
<style>
  /* CRITICAL: Visible focus indicators */
  *:focus {
    outline: 3px solid #4CAF50;
    outline-offset: 2px;
  }
  
  /* Never remove outlines without replacement! */
  /* button:focus { outline: none; }  ← BAD! */
  
  /* Custom focus style (if you must) */
  .custom-focus:focus {
    outline: none;
    box-shadow: 0 0 0 3px #4CAF50;
  }
  
  /* Skip link (hidden until focused) */
  .skip-link {
    position: absolute;
    top: -40px;
    left: 0;
    background: #000;
    color: white;
    padding: 8px;
    text-decoration: none;
    z-index: 100;
  }
  
  .skip-link:focus {
    top: 0;
  }
  
  /* Focus within for container styling */
  .nav-container:focus-within {
    background: #f0f0f0;
  }
</style>
</head>
<body>
<!-- Skip to main content link -->
<a href="#main-content" class="skip-link">
  Skip to main content
</a>

<header>
  <nav>
    <!-- Keyboard accessible menu -->
    <ul>
      <li><a href="/" accesskey="h">Home</a></li>
      <li><a href="/about">About</a></li>
      <li><a href="/contact" accesskey="c">Contact</a></li>
    </ul>
  </nav>
</header>

<main id="main-content" tabindex="-1">
  <h1>Main Content</h1>
  
  <!-- Keyboard accessible custom button -->
  <div 
    role="button"
    tabindex="0"
    onkeydown="handleKeyPress(event)"
    onclick="handleClick()">
    Custom Button (use real buttons instead!)
  </div>
  
  <!-- Proper button (automatically keyboard accessible) -->
  <button onclick="handleClick()">
    Real Button (Preferred!)
  </button>
  
  <!-- Keyboard accessible modal -->
  <button onclick="openModal()">Open Modal</button>
  
  <div 
    role="dialog"
    aria-modal="true"
    aria-labelledby="modal-title"
    id="modal"
    hidden>
    <h2 id="modal-title">Modal Title</h2>
    <p>Modal content</p>
    <button onclick="closeModal()">Close</button>
  </div>
</main>

<script>
  // Handle Enter and Space for custom button
  function handleKeyPress(event) {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      handleClick();
    }
  }
  
  function handleClick() {
    alert('Button clicked!');
  }
  
  // Trap focus in modal
  function openModal() {
    const modal = document.getElementById('modal');
    modal.hidden = false;
    
    // Focus first focusable element
    const firstFocusable = modal.querySelector('button');
    firstFocusable.focus();
    
    // Store last focused element
    window.lastFocusedElement = document.activeElement;
  }
  
  function closeModal() {
    const modal = document.getElementById('modal');
    modal.hidden = true;
    
    // Return focus to trigger element
    if (window.lastFocusedElement) {
      window.lastFocusedElement.focus();
    }
  }
  
  // Close modal on Escape
  document.addEventListener('keydown', function(event) {
    if (event.key === 'Escape') {
      const modal = document.getElementById('modal');
      if (!modal.hidden) {
        closeModal();
      }
    }
  });
</script>
</body>
</html>

Keyboard requirements:

Color Contrast and Visual Design

Ensure sufficient contrast for readability:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Color Contrast</title>
<style>
  body {
    font-family: Arial, sans-serif;
    padding: 40px;
    line-height: 1.6;
  }
  
  /* Recommended example: Sufficient contrast (4.5:1 for normal text) */
  .good-contrast {
    background: #ffffff;
    color: #333333;
    padding: 20px;
    margin-bottom: 20px;
  }
  
  /* Recommended example: High contrast (7:1 for large text) */
  .high-contrast {
    background: #000000;
    color: #ffffff;
    padding: 20px;
    margin-bottom: 20px;
    font-size: 1.5rem;
  }
  
  /* Problematic example: Poor contrast */
  .bad-contrast {
    background: #ffffff;
    color: #cccccc; /* Too light! */
    padding: 20px;
    margin-bottom: 20px;
  }
  
  /* Recommended example: Don't rely on color alone */
  .status-success {
    border-left: 4px solid #4CAF50;
    padding-left: 15px;
    background: #e8f5e9;
  }
  
  .status-success::before {
    content: '✓ ';
    font-weight: bold;
  }
  
  .status-error {
    border-left: 4px solid #f44336;
    padding-left: 15px;
    background: #ffebee;
  }
  
  .status-error::before {
    content: '✗ ';
    font-weight: bold;
  }
  
  /* Recommended example: Underline links, don't rely on color */
  a {
    color: #0066cc;
    text-decoration: underline;
  }
  
  a:hover {
    background: #e3f2fd;
  }
  
  /* Recommended example: Large touch targets (44x44px minimum) */
  button {
    min-width: 44px;
    min-height: 44px;
    padding: 12px 24px;
    font-size: 16px;
    cursor: pointer;
  }
</style>
</head>
<body>
<h1>Color Contrast Examples</h1>

<div class="good-contrast">
  <strong>Good Contrast:</strong> This text has sufficient contrast
  ratio (4.5:1) and is easy to read for most users.
</div>

<div class="high-contrast">
  <strong>High Contrast:</strong> Large text with excellent
  contrast ratio (21:1).
</div>

<div class="bad-contrast">
  <strong>Poor Contrast:</strong> This text is hard to read due to
  insufficient contrast. Avoid light gray on white!
</div>

<h2>Don't Rely on Color Alone</h2>

<div class="status-success">
  Success! Your form has been submitted. Icon and border provide
  additional cues beyond just green color.
</div>

<div class="status-error">
  Error! Please fix the form errors before submitting. Icon and
  border help color-blind users identify the error state.
</div>

<h2>Link Styling</h2>
<p>
  Links should be <a href="#">underlined and colored</a> so
  color-blind users can identify them. Hover states provide
  additional feedback.
</p>
</body>
</html>

Contrast requirements:

Form Accessibility

Make forms usable for everyone:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Accessible Forms</title>
<style>
  form {
    max-width: 600px;
    margin: 50px auto;
    padding: 30px;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
  
  .form-group {
    margin-bottom: 25px;
  }
  
  label {
    display: block;
    margin-bottom: 8px;
    font-weight: bold;
  }
  
  .required {
    color: #f44336;
  }
  
  input, textarea, select {
    width: 100%;
    padding: 12px;
    border: 2px solid #ddd;
    border-radius: 4px;
    font-size: 16px;
  }
  
  input:focus, textarea:focus, select:focus {
    outline: none;
    border-color: #4CAF50;
  }
  
  input:invalid:not(:placeholder-shown) {
    border-color: #f44336;
  }
  
  .hint {
    font-size: 0.9rem;
    color: #666;
    margin-top: 5px;
  }
  
  .error {
    color: #f44336;
    font-size: 0.9rem;
    margin-top: 5px;
    display: none;
  }
  
  input:invalid:not(:placeholder-shown) + .error {
    display: block;
  }
  
  fieldset {
    border: 2px solid #ddd;
    border-radius: 4px;
    padding: 20px;
    margin-bottom: 25px;
  }
  
  legend {
    font-weight: bold;
    padding: 0 10px;
  }
  
  .radio-group label,
  .checkbox-group label {
    display: flex;
    align-items: center;
    gap: 10px;
    font-weight: normal;
    margin-bottom: 10px;
  }
  
  button {
    padding: 12px 30px;
    background: #4CAF50;
    color: white;
    border: none;
    border-radius: 4px;
    font-size: 1rem;
    cursor: pointer;
  }
</style>
</head>
<body>
<form>
  <h1>Accessible Contact Form</h1>
  
  <!-- Text input with proper label -->
  <div class="form-group">
    <label for="name">
      Name <span class="required" aria-label="required">*</span>
    </label>
    <input 
      type="text" 
      id="name" 
      name="name"
      required
      aria-required="true"
      aria-describedby="name-hint"
      placeholder=" ">
    <div id="name-hint" class="hint">Please enter your full name</div>
    <div class="error" role="alert">Name is required</div>
  </div>
  
  <!-- Email with validation -->
  <div class="form-group">
    <label for="email">
      Email <span class="required" aria-label="required">*</span>
    </label>
    <input 
      type="email" 
      id="email" 
      name="email"
      required
      aria-required="true"
      aria-describedby="email-hint"
      placeholder=" ">
    <div id="email-hint" class="hint">We'll never share your email</div>
    <div class="error" role="alert">Valid email required</div>
  </div>
  
  <!-- Fieldset for radio buttons -->
  <fieldset>
    <legend>Preferred Contact Method</legend>
    <div class="radio-group">
      <label>
        <input 
          type="radio" 
          name="contact-method" 
          value="email"
          checked>
        Email
      </label>
      <label>
        <input 
          type="radio" 
          name="contact-method" 
          value="phone">
        Phone
      </label>
      <label>
        <input 
          type="radio" 
          name="contact-method" 
          value="mail">
        Mail
      </label>
    </div>
  </fieldset>
  
  <!-- Checkboxes with fieldset -->
  <fieldset>
    <legend>Interests</legend>
    <div class="checkbox-group">
      <label>
        <input type="checkbox" name="interests" value="web">
        Web Development
      </label>
      <label>
        <input type="checkbox" name="interests" value="design">
        Design
      </label>
      <label>
        <input type="checkbox" name="interests" value="marketing">
        Marketing
      </label>
    </div>
  </fieldset>
  
  <!-- Textarea -->
  <div class="form-group">
    <label for="message">Message</label>
    <textarea 
      id="message" 
      name="message" 
      rows="5"
      aria-describedby="message-hint"></textarea>
    <div id="message-hint" class="hint">
      Tell us how we can help you
    </div>
  </div>
  
  <button type="submit">Send Message</button>
</form>
</body>
</html>

Form accessibility checklist:

Testing for Accessibility

Test your sites with these tools and techniques:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Accessibility Testing Guide</title>
<style>
  body {
    font-family: Arial, sans-serif;
    max-width: 900px;
    margin: 50px auto;
    padding: 20px;
    line-height: 1.6;
  }
  
  .testing-method {
    background: #f8f9fa;
    padding: 25px;
    margin-bottom: 25px;
    border-radius: 8px;
    border-left: 4px solid #4CAF50;
  }
  
  .testing-method h2 {
    margin-top: 0;
    color: #333;
  }
  
  code {
    background: #e9ecef;
    padding: 2px 6px;
    border-radius: 3px;
    font-family: 'Courier New', monospace;
  }
  
  ul {
    margin: 15px 0;
  }
  
  li {
    margin-bottom: 8px;
  }
</style>
</head>
<body>
<h1>How to Test Accessibility</h1>

<div class="testing-method">
  <h2>1. Keyboard-Only Navigation</h2>
  <p><strong>Test:</strong> Unplug your mouse and navigate using only keyboard</p>
  <ul>
    <li><code>Tab</code> - Move forward through interactive elements</li>
    <li><code>Shift + Tab</code> - Move backward</li>
    <li><code>Enter</code> - Activate links and buttons</li>
    <li><code>Space</code> - Activate buttons, check checkboxes</li>
    <li><code>Arrow keys</code> - Navigate within custom widgets</li>
    <li><code>Escape</code> - Close modals and dropdowns</li>
  </ul>
  <p><strong>Check:</strong> Can you access all features? Is focus visible?</p>
</div>

<div class="testing-method">
  <h2>2. Screen Reader Testing</h2>
  <p><strong>Tools:</strong></p>
  <ul>
    <li><strong>NVDA</strong> (Windows, Free) - nvaccess.org</li>
    <li><strong>JAWS</strong> (Windows, Paid) - freedomscientific.com</li>
    <li><strong>VoiceOver</strong> (macOS/iOS, Built-in) - Cmd+F5</li>
    <li><strong>TalkBack</strong> (Android, Built-in)</li>
  </ul>
  <p><strong>Listen for:</strong></p>
  <ul>
    <li>Are headings announced correctly?</li>
    <li>Do images have alt text?</li>
    <li>Are form labels read?</li>
    <li>Is the reading order logical?</li>
  </ul>
</div>

<div class="testing-method">
  <h2>3. Automated Testing Tools</h2>
  <p><strong>Browser Extensions:</strong></p>
  <ul>
    <li><strong>axe DevTools</strong> - Free, comprehensive</li>
    <li><strong>WAVE</strong> - Visual feedback tool</li>
    <li><strong>Lighthouse</strong> - Built into Chrome DevTools</li>
  </ul>
  <p><strong>Command-line:</strong></p>
  <ul>
    <li><strong>axe-core</strong> - Automated testing library</li>
    <li><strong>pa11y</strong> - CI/CD accessibility testing</li>
  </ul>
</div>

<div class="testing-method">
  <h2>4. Color Contrast Checkers</h2>
  <ul>
    <li><strong>WebAIM Contrast Checker</strong> - webaim.org/resources/contrastchecker</li>
    <li><strong>Colour Contrast Analyzer</strong> - Desktop app</li>
    <li><strong>Chrome DevTools</strong> - Inspect element contrast</li>
  </ul>
</div>

<div class="testing-method">
  <h2>5. Vision Simulators</h2>
  <p>Test how your site appears with vision impairments:</p>
  <ul>
    <li><strong>NoCoffee</strong> - Chrome extension</li>
    <li><strong>Sim Daltonism</strong> - macOS app for color blindness</li>
    <li><strong>Chrome DevTools</strong> - Emulate vision deficiencies</li>
  </ul>
</div>

<div class="testing-method">
  <h2>6. Mobile Accessibility</h2>
  <ul>
    <li>Test with VoiceOver (iOS) or TalkBack (Android)</li>
    <li>Check touch target sizes (44x44px minimum)</li>
    <li>Test landscape and portrait orientations</li>
    <li>Verify zoom functionality (up to 200%)</li>
  </ul>
</div>

<div class="testing-method">
  <h2>7. HTML Validation</h2>
  <ul>
    <li><strong>W3C Validator</strong> - validator.w3.org</li>
    <li><strong>HTML5 Outliner</strong> - Check heading structure</li>
  </ul>
  <p>Valid HTML is the foundation of accessibility!</p>
</div>
</body>
</html>

Common Accessibility Mistakes

Mistake 1: Removing Focus Outlines

Problematic example:

*:focus { outline: none; }

Improved example:

*:focus {
  outline: 3px solid #4CAF50;
  outline-offset: 2px;
}

Why: Keyboard users need to see where they are!

Mistake 2: Using Divs for Buttons

Problematic example:

<div onclick="submit()">Submit</div>

Improved example:

<button type="submit">Submit</button>

Why: Buttons are keyboard accessible by default.

Problematic example:

<a href="/profile"><img src="avatar.jpg"></a>

Improved example:

<a href="/profile">
  <img src="avatar.jpg" alt="View profile">
</a>

Why: Screen readers need text content or alt text.

Mistake 4: Poor Heading Structure

Problematic example:

<h1>Title</h1>
<h4>Subtitle</h4>

Improved example:

<h1>Title</h1>
<h2>Subtitle</h2>

Why: Don’t skip heading levels.

Try It Yourself

Ready to build accessible sites? Try these challenges:

Challenge 1: Accessible Navigation (Beginner)

Create a navigation menu with:

Challenge 2: Accessible Form (Intermediate)

Build a complete form featuring:

Challenge 3: Accessible Widget Library (Advanced)

Create accessible components:

Bonus: Test everything with a screen reader!

What You Learned

Congratulations! You now know how to:

Next Steps

Now that you understand accessibility, explore these related tutorials:

Ready to build accessible websites? Start creating in our interactive HTML editor!

Back to all tutorials