Accessibility is not optional. In 2026, web accessibility lawsuits continue to rise, regulatory requirements are expanding, and — most importantly — 1 billion people worldwide live with disabilities. Building accessible products is both a legal requirement and the right thing to do.

This guide covers practical accessibility implementation for web developers.

Accessibility WCAG Accessible design benefits everyone, not just users with disabilities

Understanding WCAG

WCAG (Web Content Accessibility Guidelines) is the international standard for web accessibility.

WCAG 2.2 Structure

WCAG is organized around four principles (POUR):

Principle Meaning
Perceivable Information must be presentable in ways users can perceive
Operable Interface components must be operable by all users
Understandable Information and UI operation must be understandable
Robust Content must be robust enough to work with assistive technologies

Conformance Levels

Level Description Required For
A Minimum accessibility Basic compliance
AA Standard accessibility Most regulations (ADA, EU)
AAA Enhanced accessibility Specialized applications

Target Level AA. It covers most accessibility needs and is the regulatory standard.

Practical Implementation

Semantic HTML

Use the right HTML elements. This solves 50% of accessibility issues:

<!-- Bad: div-based navigation -->
<div class="nav">
  <div class="nav-item" onclick="navigate('/home')">Home</div>
</div>

<!-- Good: semantic navigation -->
<nav aria-label="Main navigation">
  <ul>
    <li><a href="/home">Home</a></li>
  </ul>
</nav>

Key semantic elements:

Element Purpose
<header> Page or section header
<nav> Navigation links
<main> Main content
<article> Self-contained content
<section> Thematic grouping
<aside> Tangentially related content
<footer> Page or section footer
<button> Clickable actions
<a> Navigation links

Keyboard Navigation

All functionality must be keyboard accessible:

// Component with keyboard support
function Dropdown({ items, onSelect }) {
  const [activeIndex, setActiveIndex] = useState(0)
  const [isOpen, setIsOpen] = useState(false)

  const handleKeyDown = (e: KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault()
        setActiveIndex(prev => Math.min(prev + 1, items.length - 1))
        break
      case 'ArrowUp':
        e.preventDefault()
        setActiveIndex(prev => Math.max(prev - 1, 0))
        break
      case 'Enter':
      case ' ':
        e.preventDefault()
        onSelect(items[activeIndex])
        setIsOpen(false)
        break
      case 'Escape':
        setIsOpen(false)
        break
    }
  }

  return (
    <div role="listbox" onKeyDown={handleKeyDown}>
      {items.map((item, index) => (
        <div
          key={item.id}
          role="option"
          aria-selected={index === activeIndex}
          tabIndex={index === activeIndex ? 0 : -1}
        >
          {item.label}
        </div>
      ))}
    </div>
  )
}

Keyboard requirements:

  • All interactive elements must be focusable
  • Focus order must be logical
  • Focus indicator must be visible
  • No keyboard traps (user can always navigate away)

ARIA (When Needed)

ARIA supplements HTML when semantics are not enough:

<!-- Custom dropdown needs ARIA -->
<button
  aria-haspopup="listbox"
  aria-expanded="true"
  aria-controls="dropdown-list"
>
  Select option
</button>

<ul
  id="dropdown-list"
  role="listbox"
  aria-label="Options"
>
  <li role="option" aria-selected="true">Option 1</li>
  <li role="option" aria-selected="false">Option 2</li>
</ul>

ARIA rules:

  1. Do not use ARIA if native HTML works
  2. All interactive ARIA elements need keyboard support
  3. Never change native semantics unnecessarily
  4. All ARIA controls need accessible names

Color and Contrast

WCAG requires minimum contrast ratios:

Content Ratio (AA) Ratio (AAA)
Normal text 4.5:1 7:1
Large text (18pt+) 3:1 4.5:1
UI components 3:1
/* Bad: low contrast */
.text {
  color: #999999;  /* 2.85:1 on white - fails AA */
}

/* Good: sufficient contrast */
.text {
  color: #595959;  /* 7:1 on white - passes AAA */
}

Tools: Use contrast checkers in your design tools or browser DevTools.

Images and Media

Provide text alternatives:

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

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

<!-- Complex image with detailed description -->
<figure>
  <img
    src="complex-diagram.png"
    alt="System architecture overview"
    aria-describedby="diagram-description"
  />
  <figcaption id="diagram-description">
    The system consists of three layers: presentation (React),
    API (Node.js), and data (PostgreSQL). Traffic flows through
    a load balancer to multiple API instances...
  </figcaption>
</figure>

Video requirements:

  • Captions for deaf/hard of hearing users
  • Audio descriptions for blind users
  • Transcripts for all users

Forms

Forms are common accessibility failure points:

<!-- Accessible form -->
<form>
  <div class="form-group">
    <label for="email">
      Email address
      <span aria-hidden="true">*</span>
    </label>
    <input
      id="email"
      type="email"
      required
      aria-required="true"
      aria-describedby="email-hint email-error"
    />
    <span id="email-hint" class="hint">
      We'll never share your email
    </span>
    <span id="email-error" class="error" role="alert">
      Please enter a valid email address
    </span>
  </div>

  <fieldset>
    <legend>Notification preferences</legend>
    <div>
      <input type="checkbox" id="email-notifications" />
      <label for="email-notifications">Email notifications</label>
    </div>
    <div>
      <input type="checkbox" id="sms-notifications" />
      <label for="sms-notifications">SMS notifications</label>
    </div>
  </fieldset>

  <button type="submit">Save preferences</button>
</form>

Form requirements:

  • Every input has a visible label
  • Labels are programmatically associated (for/id)
  • Error messages are clear and announced
  • Required fields are indicated
  • Group related inputs with fieldset/legend

Focus Management

Manage focus for dynamic content:

// Modal focus management
function Modal({ isOpen, onClose, children }) {
  const modalRef = useRef<HTMLDivElement>(null)
  const previousFocus = useRef<HTMLElement | null>(null)

  useEffect(() => {
    if (isOpen) {
      // Store current focus
      previousFocus.current = document.activeElement as HTMLElement
      // Move focus to modal
      modalRef.current?.focus()
    } else {
      // Restore focus when closing
      previousFocus.current?.focus()
    }
  }, [isOpen])

  if (!isOpen) return null

  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      aria-labelledby="modal-title"
      tabIndex={-1}
    >
      <FocusTrap>
        <h2 id="modal-title">Modal Title</h2>
        {children}
        <button onClick={onClose}>Close</button>
      </FocusTrap>
    </div>
  )
}

Live Regions

Announce dynamic content to screen readers:

<!-- Polite announcement (waits for pause) -->
<div aria-live="polite" aria-atomic="true">
  Item added to cart
</div>

<!-- Assertive announcement (interrupts) -->
<div role="alert">
  Error: Payment failed. Please try again.
</div>

<!-- Status updates -->
<div role="status">
  Loading... 50% complete
</div>

Testing Accessibility

Automated Testing

Automated tools catch ~30% of issues:

# axe-core with Playwright
npm install @axe-core/playwright

# In tests
import { injectAxe, checkA11y } from 'axe-playwright'

test('homepage is accessible', async ({ page }) => {
  await page.goto('/')
  await injectAxe(page)
  await checkA11y(page)
})

Tools:

  • axe-core (browser extension, CI integration)
  • Lighthouse (Chrome DevTools)
  • WAVE (browser extension)
  • pa11y (command line)

Manual Testing

Automated testing is not enough:

Keyboard testing:

  1. Tab through entire page
  2. Verify focus is visible
  3. Verify all interactions work without mouse
  4. Check for keyboard traps

Screen reader testing:

  • VoiceOver (macOS, iOS)
  • NVDA (Windows, free)
  • JAWS (Windows, paid)

Other checks:

  • Zoom to 200% — does content reflow?
  • Disable CSS — does content make sense?
  • Check color contrast
  • Verify heading hierarchy

Accessibility Audit Checklist

Category Check
Structure Semantic HTML used
One h1 per page
Logical heading hierarchy
Landmarks present (main, nav, etc.)
Keyboard All elements focusable
Focus visible
No keyboard traps
Skip link present
Images Alt text for informative images
Decorative images marked
Forms Labels associated
Errors announced
Required fields indicated
Color Contrast ratios met
Information not color-only
Motion Respects prefers-reduced-motion
No auto-playing media

Component Patterns

<body>
  <a href="#main-content" class="skip-link">
    Skip to main content
  </a>
  <nav>...</nav>
  <main id="main-content" tabindex="-1">
    ...
  </main>
</body>

<style>
.skip-link {
  position: absolute;
  left: -9999px;
}
.skip-link:focus {
  left: 0;
  z-index: 9999;
  background: white;
  padding: 1rem;
}
</style>

Accessible Tabs

function Tabs({ tabs }) {
  const [activeTab, setActiveTab] = useState(0)

  return (
    <div>
      <div role="tablist" aria-label="Content tabs">
        {tabs.map((tab, index) => (
          <button
            key={tab.id}
            role="tab"
            id={`tab-${tab.id}`}
            aria-selected={index === activeTab}
            aria-controls={`panel-${tab.id}`}
            tabIndex={index === activeTab ? 0 : -1}
            onClick={() => setActiveTab(index)}
          >
            {tab.label}
          </button>
        ))}
      </div>
      {tabs.map((tab, index) => (
        <div
          key={tab.id}
          role="tabpanel"
          id={`panel-${tab.id}`}
          aria-labelledby={`tab-${tab.id}`}
          hidden={index !== activeTab}
        >
          {tab.content}
        </div>
      ))}
    </div>
  )
}

Making It Sustainable

Build Accessibility Into Process

  1. Design phase: Accessibility review of mockups
  2. Development: Use accessible components, write semantic HTML
  3. Code review: Check for accessibility issues
  4. Testing: Automated + manual accessibility tests
  5. QA: Include accessibility in test plans

Use Accessible Component Libraries

Start with components that handle accessibility:

  • Radix UI
  • Headless UI
  • React Aria
  • Chakra UI

These provide keyboard support, ARIA, and focus management out of the box.

Educate Your Team

Accessibility is everyone's responsibility:

  • Designers: Color contrast, focus states, touch targets
  • Developers: Semantic HTML, keyboard support, ARIA
  • QA: Testing with assistive technologies
  • Content: Clear writing, alt text, captions

The Bottom Line

Accessibility is not a feature — it is a quality standard. Just as you would not ship code that crashes for some users, you should not ship code that is unusable for users with disabilities.

Start with semantic HTML. Test with a keyboard. Run automated scans. Gradually expand your skills. Perfect accessibility is a journey, but every step makes your product usable by more people.

Comments