Skip to content

Building Accessible Web Applications: A Developer's Guide

Learn how to create inclusive web applications that work for everyone, including users with disabilities. Discover practical accessibility techniques and WCAG guidelines.

Author
Navinda Chandrasiri
Published
Reading time
7 min read
Topics
AccessibilityWeb DevelopmentWCAGReactHTML
01
On this page

Web accessibility isn't just about compliance - it's about creating experiences that work for everyone. Let's explore how to build truly accessible web applications.

Why Accessibility Matters#

  • 1 billion people worldwide have some form of disability
  • Accessibility improves UX for all users, not just those with disabilities
  • Many countries have legal requirements for web accessibility
  • Accessible sites often have better SEO and performance

The Four Principles of WCAG#

The Web Content Accessibility Guidelines (WCAG) are built on four principles, remembered by the acronym POUR:

1. Perceivable#

Users must be able to perceive the information being presented.

tsx
// ✅ Good: Proper image alt text
<img
  src="/product.jpg"
  alt="Blue running shoes with white laces"
/>
 
// ❌ Bad: Missing or poor alt text
<img src="/product.jpg" alt="image" />
 
// For decorative images
<img src="/decoration.png" alt="" role="presentation" />

2. Operable#

Users must be able to operate the interface.

tsx
// ✅ Good: Keyboard accessible
<button onClick={handleClick}>
  Click me
</button>
 
// ❌ Bad: Not keyboard accessible
<div onClick={handleClick}>
  Click me
</div>

3. Understandable#

Information and operation must be understandable.

tsx
// ✅ Good: Clear form labels
<label htmlFor="email">
  Email Address
  <input
    id="email"
    type="email"
    aria-required="true"
  />
</label>
 
// ❌ Bad: No label
<input type="email" placeholder="Email" />

4. Robust#

Content must be robust enough to work with various technologies.

tsx
// ✅ Good: Semantic HTML
<nav aria-label="Main navigation">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>
 
// ❌ Bad: Non-semantic HTML
<div className="nav">
  <div><span onClick={goHome}>Home</span></div>
</div>

Essential Accessibility Techniques#

1. Semantic HTML#

Use the right HTML elements for their intended purpose:

html
<!-- ✅ Good: Semantic elements -->
<header>
  <nav>
    <ul>
      <li><a href="/">Home</a></li>
    </ul>
  </nav>
</header>
<main>
  <article>
    <h1>Article Title</h1>
    <p>Content...</p>
  </article>
</main>
<footer>
  <p>Copyright 2024</p>
</footer>
 
<!-- ❌ Bad: Div soup -->
<div class="header">
  <div class="nav">
    <div class="link">Home</div>
  </div>
</div>

2. ARIA Attributes#

Use ARIA attributes to provide additional context:

tsx
// Modal dialog
<div
  role="dialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-description"
  aria-modal="true"
>
  <h2 id="dialog-title">Confirm Action</h2>
  <p id="dialog-description">
    Are you sure you want to proceed?
  </p>
  <button>Confirm</button>
  <button>Cancel</button>
</div>
 
// Loading state
<button
  disabled
  aria-busy="true"
  aria-label="Loading, please wait"
>
  <span aria-hidden="true">⏳</span>
  Loading...
</button>
 
// Live regions for dynamic content
<div
  role="status"
  aria-live="polite"
  aria-atomic="true"
>
  {statusMessage}
</div>

3. Keyboard Navigation#

Ensure all interactive elements are keyboard accessible:

tsx
function DropdownMenu() {
  const [isOpen, setIsOpen] = useState(false)
 
  const handleKeyDown = (e: KeyboardEvent) => {
    switch (e.key) {
      case 'Escape':
        setIsOpen(false)
        break
      case 'ArrowDown':
        // Focus next item
        break
      case 'ArrowUp':
        // Focus previous item
        break
    }
  }
 
  return (
    <div>
      <button
        aria-expanded={isOpen}
        aria-haspopup="true"
        onClick={() => setIsOpen(!isOpen)}
      >
        Menu
      </button>
      {isOpen && (
        <ul role="menu" onKeyDown={handleKeyDown}>
          <li role="menuitem" tabIndex={0}>Item 1</li>
          <li role="menuitem" tabIndex={0}>Item 2</li>
        </ul>
      )}
    </div>
  )
}

4. Focus Management#

Manage focus appropriately:

tsx
function Modal({ isOpen, onClose }) {
  const modalRef = useRef<HTMLDivElement>(null)
  const previousFocusRef = useRef<HTMLElement>()
 
  useEffect(() => {
    if (isOpen) {
      // Save current focus
      previousFocusRef.current = document.activeElement as HTMLElement
      // Focus modal
      modalRef.current?.focus()
    } else {
      // Restore previous focus
      previousFocusRef.current?.focus()
    }
  }, [isOpen])
 
  return (
    <div
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      tabIndex={-1}
    >
      {/* Modal content */}
    </div>
  )
}

5. Color Contrast#

Ensure sufficient color contrast:

css
/* ✅ Good: WCAG AA compliant (4.5:1 for normal text) */
.text {
  color: #333333;
  background-color: #ffffff;
}
 
/* ❌ Bad: Insufficient contrast */
.text {
  color: #cccccc;
  background-color: #ffffff;
}
 
/* Don't rely solely on color to convey information */
.error {
  color: #dc3545;
  /* Add icon or text indicator */
}

6. Form Accessibility#

Create accessible forms:

tsx
function AccessibleForm() {
  return (
    <form aria-labelledby="form-title">
      <h2 id="form-title">Contact Form</h2>
 
      {/* Text input */}
      <div>
        <label htmlFor="name">
          Name <span aria-label="required">*</span>
        </label>
        <input
          id="name"
          type="text"
          required
          aria-required="true"
          aria-describedby="name-hint"
        />
        <small id="name-hint">
          Enter your full name
        </small>
      </div>
 
      {/* Error state */}
      <div>
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          aria-invalid="true"
          aria-describedby="email-error"
        />
        <div id="email-error" role="alert">
          Please enter a valid email address
        </div>
      </div>
 
      {/* Submit */}
      <button type="submit">
        Submit Form
      </button>
    </form>
  )
}

Testing for Accessibility#

Automated Tools#

  1. axe DevTools - Browser extension for finding accessibility issues
  2. Lighthouse - Built into Chrome DevTools
  3. WAVE - Web Accessibility Evaluation Tool
  4. eslint-plugin-jsx-a11y - ESLint rules for React

Manual Testing#

  1. Keyboard Navigation: Can you navigate without a mouse?
  2. Screen Reader: Test with NVDA (Windows) or VoiceOver (Mac)
  3. Zoom: Test at 200% zoom
  4. Color Blindness: Use a color blindness simulator

Testing Checklist#

  • All images have appropriate alt text
  • Forms have proper labels and error messages
  • Interactive elements are keyboard accessible
  • Color contrast meets WCAG standards
  • Page structure uses semantic HTML
  • Dynamic content updates are announced
  • Focus is managed appropriately
  • No keyboard traps exist

Common Accessibility Mistakes#

1. DIV and SPAN Overuse#

tsx
// ❌ Bad
<div onClick={handleClick}>Click me</div>
 
// ✅ Good
<button onClick={handleClick}>Click me</button>

2. Missing Alternative Text#

tsx
// ❌ Bad
<img src="chart.png" />
 
// ✅ Good
<img src="chart.png" alt="Sales increased 25% in Q4" />
tsx
// ❌ Bad
<a href="/article">Click here</a>
 
// ✅ Good
<a href="/article">Read the full accessibility guide</a>

4. Inaccessible Custom Components#

tsx
// ❌ Bad
<div className="checkbox" onClick={toggle}>
  {checked && '✓'}
</div>
 
// ✅ Good
<input
  type="checkbox"
  checked={checked}
  onChange={toggle}
  aria-label="Accept terms"
/>

Conclusion#

Building accessible web applications is not optional - it's a fundamental part of professional web development. By following WCAG guidelines, using semantic HTML, and testing with real users, you can create experiences that work for everyone.

Remember: Accessibility is an ongoing process, not a one-time fix. Regular audits, user testing, and staying current with best practices will help you maintain and improve accessibility over time.

Resources#

Let's build a more inclusive web, together!

Portrait of Navinda Chandrasiri

Written by Navinda Chandrasiri

Senior Software Engineer in Colombo, Sri Lanka, building cloud-native web applications and AI systems.

See my portfolio

ShareLinkedInX

  1. Getting Started with Vite v7: A Modern Frontend Setup Guide

    An in-depth introduction to Vite v7, covering framework selection, React variants, and modern tooling choices for fast frontend development.

    ViteReactFrontendTypeScript

    9 min read
  2. Getting Started with Next.js 14: A Complete Guide

    Learn how to build modern web applications with Next.js 14, featuring the new App Router, Server Components, and improved performance.

    Next.jsReactTypeScriptWeb Development

    8 min read