Web Accessibility: A Developer's Guide

Reverend Philip Dec 9, 2025 8 min read

Build inclusive web applications. Learn WCAG guidelines, semantic HTML, ARIA attributes, and testing for accessibility.

Web accessibility ensures that websites work for everyone, including people with disabilities. Beyond being the right thing to do, accessibility is often legally required and improves the experience for all users. This guide covers practical techniques for building accessible web applications.

Why Accessibility Matters

Who Benefits

  • Visual impairments: Blindness, low vision, color blindness
  • Motor impairments: Limited fine motor control, tremors
  • Hearing impairments: Deafness, hard of hearing
  • Cognitive impairments: Dyslexia, ADHD, memory issues
  • Temporary impairments: Broken arm, bright sunlight, loud environment
  • Situational limitations: One hand occupied, small screen

Business Case

  • Legal compliance (ADA, Section 508, EU Directive)
  • Larger audience reach
  • Better SEO (semantic HTML)
  • Improved usability for everyone

WCAG Guidelines

The Web Content Accessibility Guidelines (WCAG) define accessibility standards:

Four Principles (POUR)

  1. Perceivable: Users can perceive the content
  2. Operable: Users can navigate and interact
  3. Understandable: Content is clear and predictable
  4. Robust: Content works across technologies

Conformance Levels

  • Level A: Minimum accessibility
  • Level AA: Addresses major barriers (most common target)
  • Level AAA: Highest level of accessibility

Semantic HTML

Use Elements for Their Purpose

Native HTML elements come with built-in accessibility features. Buttons are focusable, announce their role to screen readers, and respond to keyboard events. When you use divs instead, you have to recreate all of this manually.

<!-- Bad: Divs for everything -->
<div class="button" onclick="submit()">Submit</div>
<div class="nav">
  <div class="nav-item">Home</div>
</div>

<!-- Good: Semantic elements -->
<button type="submit">Submit</button>
<nav>
  <a href="/">Home</a>
</nav>

Document Structure

Landmark elements help screen reader users navigate your page efficiently. They can jump directly to the main content, navigation, or footer without having to listen to everything in between.

<header>
  <nav aria-label="Main navigation">...</nav>
</header>

<main>
  <article>
    <h1>Page Title</h1>
    <section>
      <h2>Section Title</h2>
      <p>Content...</p>
    </section>
  </article>
</main>

<aside aria-label="Related articles">...</aside>

<footer>...</footer>

The aria-label attribute distinguishes between multiple navigation regions, which is helpful when you have both primary and secondary navigation.

Heading Hierarchy

Headings create an outline of your page that screen reader users can navigate. Skipping levels breaks this outline and makes it harder to understand the page structure.

<!-- Bad: Skipping levels -->
<h1>Main Title</h1>
<h3>Subsection</h3>

<!-- Good: Proper hierarchy -->
<h1>Main Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>

Keyboard Navigation

Focus Management

Focus indicators show keyboard users where they are on the page. Removing them without providing a replacement makes your site nearly unusable for people who don't use a mouse.

/* Never remove focus outlines without replacement */
/* Bad */
*:focus { outline: none; }

/* Good - custom focus styles */
*:focus {
  outline: 2px solid #4A90D9;
  outline-offset: 2px;
}

/* Or use focus-visible for mouse users */
*:focus:not(:focus-visible) {
  outline: none;
}
*:focus-visible {
  outline: 2px solid #4A90D9;
}

The focus-visible pseudo-class shows focus styles only for keyboard navigation, hiding them for mouse clicks while maintaining accessibility.

Tab Order

The tab order should follow the visual reading order of your page. Using tabindex values greater than 0 can create confusing navigation patterns.

<!-- Logical tab order follows visual order -->
<form>
  <label for="name">Name</label>
  <input type="text" id="name">

  <label for="email">Email</label>
  <input type="email" id="email">

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

<!-- Use tabindex sparingly -->
<div tabindex="0">Focusable div (only when necessary)</div>
<div tabindex="-1">Programmatically focusable only</div>
<!-- Avoid tabindex > 0 -->

Keyboard Interactions

Custom interactive components need keyboard event handlers. Users expect standard key behaviors: Enter and Space to activate, Escape to close, and arrow keys to navigate within widgets.

// Custom components need keyboard support
element.addEventListener('keydown', (e) => {
  switch(e.key) {
    case 'Enter':
    case ' ':
      e.preventDefault();
      this.activate();
      break;
    case 'Escape':
      this.close();
      break;
    case 'ArrowDown':
      e.preventDefault();
      this.focusNext();
      break;
  }
});

The preventDefault calls stop the browser's default behavior, such as scrolling when Space is pressed.

ARIA Attributes

ARIA (Accessible Rich Internet Applications) adds accessibility info to HTML.

When to Use ARIA

First rule: Don't use ARIA if native HTML works.

Native elements are always more robust than ARIA-enhanced divs. A real button works everywhere; an ARIA button requires JavaScript and careful implementation to match native behavior.

<!-- Don't do this -->
<div role="button" tabindex="0" aria-pressed="false">Toggle</div>

<!-- Do this -->
<button aria-pressed="false">Toggle</button>

Common ARIA Attributes

ARIA attributes communicate state and relationships that aren't expressed in the DOM structure. Use them to label interactive elements, describe requirements, and indicate dynamic states.

<!-- Labels -->
<button aria-label="Close dialog">×</button>
<input aria-labelledby="name-label name-hint">

<!-- Descriptions -->
<input aria-describedby="password-requirements">
<div id="password-requirements">Must be 8+ characters</div>

<!-- States -->
<button aria-expanded="false">Menu</button>
<div aria-hidden="true">Decorative content</div>
<input aria-invalid="true" aria-errormessage="email-error">

<!-- Live regions -->
<div aria-live="polite">Status updates appear here</div>
<div aria-live="assertive">Critical alerts</div>

<!-- Roles -->
<div role="alert">Error message</div>
<nav role="navigation" aria-label="Primary">...</nav>

The aria-live attribute tells screen readers to announce content changes. Use "polite" for non-urgent updates and "assertive" for critical alerts that should interrupt.

ARIA for Dynamic Content

When JavaScript updates the page, screen reader users may not notice the change. Live regions and ARIA states ensure dynamic updates are communicated.

// Announce dynamic changes
const statusRegion = document.getElementById('status');
statusRegion.setAttribute('aria-live', 'polite');

function updateStatus(message) {
  statusRegion.textContent = message;
}

// For loading states
button.setAttribute('aria-busy', 'true');
button.textContent = 'Loading...';

Forms

Labels and Instructions

Every form field needs an associated label. Screen readers read the label when the field receives focus, so users know what information to enter.

<form>
  <!-- Explicit label association -->
  <label for="email">Email address</label>
  <input type="email" id="email" required aria-describedby="email-hint">
  <span id="email-hint" class="hint">We'll never share your email</span>

  <!-- Group related inputs -->
  <fieldset>
    <legend>Shipping address</legend>
    <label for="street">Street</label>
    <input type="text" id="street">
    <!-- More fields -->
  </fieldset>
</form>

The fieldset and legend elements group related fields together. Screen readers announce the legend when users enter the fieldset, providing context for the fields inside.

Error Handling

Error messages need to be associated with their fields so screen readers can announce them. The role="alert" ensures the error is announced immediately when it appears.

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

When form validation fails, move focus to the first error field. This helps keyboard users find and fix problems without searching the entire form.

// Focus first error on submit
form.addEventListener('submit', (e) => {
  const firstError = form.querySelector('[aria-invalid="true"]');
  if (firstError) {
    e.preventDefault();
    firstError.focus();
  }
});

Images and Media

Alt Text

Alt text should convey the same information that sighted users get from the image. For decorative images, use an empty alt attribute so screen readers skip them entirely.

<!-- Informative images -->
<img src="chart.png" alt="Sales increased 25% from Q1 to Q2 2024">

<!-- Decorative images -->
<img src="decoration.png" alt="" role="presentation">

<!-- Complex images -->
<figure>
  <img src="diagram.png" alt="System architecture diagram">
  <figcaption>
    Full description of the architecture...
  </figcaption>
</figure>

For complex images like charts or diagrams, provide a longer description in the surrounding text or a linked document.

Video and Audio

Captions make video accessible to deaf and hard-of-hearing users. Audio descriptions narrate visual content for blind users, describing actions, expressions, and scene changes.

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

Color and Contrast

Contrast Ratios

WCAG AA requirements:

  • Normal text: 4.5:1
  • Large text (18pt+ or 14pt bold): 3:1
  • UI components: 3:1

Low contrast makes text difficult to read for users with low vision, and can be impossible to read in bright lighting conditions.

/* Good contrast */
.button {
  background-color: #1a56db;
  color: #ffffff; /* 8.59:1 ratio */
}

/* Check with tools like WebAIM Contrast Checker */

Don't Rely on Color Alone

Color-blind users may not be able to distinguish between red and green error/success states. Always provide additional visual cues like icons or text.

<!-- Bad: Only color indicates error -->
<input class="error-red">

<!-- Good: Multiple indicators -->
<input class="error" aria-invalid="true">
<span class="error-icon" aria-hidden="true">!</span>
<span class="error-message">This field is required</span>

Focus Trapping for Modals

When a modal opens, keyboard users need to be trapped inside it until they close it. Without focus trapping, tabbing will move focus to elements behind the modal that aren't visible.

class Modal {
  open() {
    this.previousFocus = document.activeElement;
    this.modal.removeAttribute('hidden');
    this.modal.setAttribute('aria-modal', 'true');

    // Move focus to modal
    this.modal.querySelector('[autofocus]')?.focus();

    // Trap focus
    this.modal.addEventListener('keydown', this.handleKeydown);
  }

  close() {
    this.modal.setAttribute('hidden', '');
    this.modal.removeAttribute('aria-modal');
    this.previousFocus?.focus();
  }

  handleKeydown = (e) => {
    if (e.key === 'Escape') {
      this.close();
      return;
    }

    if (e.key !== 'Tab') return;

    const focusable = this.modal.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  }
}

Key details: save the previously focused element so you can restore it when the modal closes, and cycle focus from the last element back to the first (and vice versa with Shift+Tab).

Testing Accessibility

Automated Tools

  • axe DevTools: Browser extension for automated testing
  • WAVE: Web accessibility evaluation tool
  • Lighthouse: Built into Chrome DevTools

Manual Testing

  1. Keyboard-only: Navigate without mouse
  2. Screen reader: Test with NVDA (Windows), VoiceOver (Mac)
  3. Zoom: Test at 200% zoom
  4. Color blindness simulators: Check color contrast

Testing Checklist

Use this checklist as a starting point for manual accessibility testing. Automated tools catch many issues, but manual testing with real assistive technologies is essential.

- [ ] All interactive elements are keyboard accessible
- [ ] Focus order is logical
- [ ] Focus is visible
- [ ] Images have appropriate alt text
- [ ] Form fields have labels
- [ ] Error messages are announced
- [ ] Color contrast meets WCAG AA
- [ ] Page works at 200% zoom
- [ ] Headings follow hierarchy
- [ ] ARIA is used correctly

Laravel/Blade Patterns

When building reusable components in Laravel, bake accessibility into the component itself. This ensures every instance of the component is accessible without developers needing to remember ARIA attributes.

{{-- Accessible form component --}}
@props(['name', 'label', 'type' => 'text', 'error' => null])

<div class="form-group">
    <label for="{{ $name }}">{{ $label }}</label>
    <input
        type="{{ $type }}"
        id="{{ $name }}"
        name="{{ $name }}"
        @if($error)
            aria-invalid="true"
            aria-describedby="{{ $name }}-error"
        @endif
        {{ $attributes }}
    >
    @if($error)
        <span id="{{ $name }}-error" class="error" role="alert">
            {{ $error }}
        </span>
    @endif
</div>

{{-- Usage --}}
<x-form-input name="email" label="Email Address" :error="$errors->first('email')" />

The component handles label association, error state, and ARIA attributes automatically. Developers using this component get accessibility for free.

Conclusion

Accessibility is not an afterthought;it's a fundamental aspect of quality web development. Start with semantic HTML, ensure keyboard navigability, provide proper labels and ARIA attributes, maintain sufficient color contrast, and test with real assistive technologies. Building accessible applications from the start is far easier than retrofitting accessibility later.

Share this article

Related Articles

Distributed Locking Patterns

Coordinate access to shared resources across services. Implement distributed locks with Redis, ZooKeeper, and databases.

Jan 16, 2026

API Design First Development

Design APIs before implementing them. Use OpenAPI specifications, mock servers, and contract-first workflows.

Jan 15, 2026

Need help with your project?

Let's discuss how we can help you build reliable software.