Web Accessibility Fundamentals: Building for Everyone

Medium•

Accessibility isn't optional - it's essential for inclusive products and legal compliance.

Quick Decision Guide

Accessibility Essentials:

1. Semantic HTML: Use <button>, <nav>, <main> not <div> 2. Alt text: Describe all images 3. Keyboard navigation: Tab, Enter, Escape work 4. Color contrast: 4.5:1 minimum for text 5. ARIA: Add labels when HTML isn't enough

Tools: axe DevTools, Lighthouse, WAVE

Result: Inclusive product, legal compliance, better UX for all.

Semantic HTML

Use Proper Elements

<!-- āŒ Bad: Non-semantic -->
<div class="header">
  <div class="nav">
    <div onclick="navigate()">Home</div>
    <div onclick="navigate()">About</div>
  </div>
</div>
<div class="main">
  <div class="article">
    <div class="title">Article Title</div>
    <div class="content">...</div>
  </div>
</div>

<!-- āœ… Good: Semantic -->
<header>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
</header>
<main>
  <article>
    <h1>Article Title</h1>
    <p>...</p>
  </article>
</main>

Benefits

•Screen readers understand structure
•Better SEO
•Easier keyboard navigation
•More maintainable code

Key Elements

<header>      <!-- Site/page header -->
<nav>         <!-- Navigation -->
<main>        <!-- Main content (one per page) -->
<article>     <!-- Self-contained content -->
<section>     <!-- Thematic grouping -->
<aside>       <!-- Tangential content -->
<footer>      <!-- Footer -->

<h1> to <h6>  <!-- Headings (hierarchy matters) -->
<button>      <!-- Interactive buttons -->
<a>           <!-- Links -->
<form>        <!-- Forms -->
<label>       <!-- Form labels -->

Alternative Text & Labels

Images

<!-- āŒ Bad: No alt -->
<img src="product.jpg">

<!-- āœ… Good: Descriptive alt -->
<img src="product.jpg" alt="Red leather handbag with gold clasp">

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

<!-- Complex images -->
<img 
  src="chart.png" 
  alt="Sales chart showing 30% increase in Q4"
  longdesc="#chart-description"
>
<div id="chart-description">
  Detailed description of the chart data...
</div>

Form Labels

<!-- āŒ Bad: No label -->
<input type="email" placeholder="Email">

<!-- āœ… Good: Explicit label -->
<label htmlFor="email">Email Address</label>
<input id="email" type="email" />

<!-- Alternative: Wrapping label -->
<label>
  Email Address
  <input type="email" />
</label>

<!-- aria-label when visual label isn't needed -->
<button aria-label="Close dialog">Ɨ</button>

Icons

<!-- āŒ Bad: Icon without text -->
<button>
  <TrashIcon />
</button>

<!-- āœ… Good: Visible text -->
<button>
  <TrashIcon /> Delete
</button>

<!-- āœ… Good: aria-label -->
<button aria-label="Delete item">
  <TrashIcon />
</button>

Keyboard Navigation

Focus Management

/* āŒ Bad: Removing focus outline */
button:focus {
  outline: none; /* Never do this! */
}

/* āœ… Good: Custom but visible focus */
button:focus {
  outline: 2px solid #0070f3;
  outline-offset: 2px;
}

/* Modern focus-visible (only for keyboard) */
button:focus-visible {
  outline: 2px solid #0070f3;
  outline-offset: 2px;
}

Tab Order

<!-- Natural tab order (default) -->
<button>First</button>
<button>Second</button>
<button>Third</button>

<!-- āŒ Bad: Forcing unusual order -->
<button tabindex="3">Third</button>
<button tabindex="1">First</button>
<button tabindex="2">Second</button>

<!-- āœ… Good: tabindex for non-interactive elements -->
<div tabindex="0" role="button" onClick={handler}>
  Click me
</div>

<!-- tabindex="-1" for programmatic focus only -->
<div tabindex="-1" ref={modalRef}>
  Modal content (focused when opened)
</div>

Keyboard Event Handlers

// āŒ Bad: Only mouse events
<div onClick={handleClick}>Click me</div>

// āœ… Good: Keyboard support
<div 
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      e.preventDefault();
      handleClick();
    }
  }}
>
  Click me
</div>

// āœ… Better: Use button element
<button onClick={handleClick}>Click me</button>

Color Contrast & Visual Design

WCAG Contrast Requirements

Level AA (Minimum):
- Normal text: 4.5:1
- Large text (18pt+): 3:1

Level AAA (Enhanced):
- Normal text: 7:1
- Large text: 4.5:1

Examples

/* āŒ Bad: Low contrast (2.1:1) */
.text {
  color: #999;  /* Light gray */
  background: #fff;  /* White */
}

/* āœ… Good: High contrast (7:1) */
.text {
  color: #333;  /* Dark gray */
  background: #fff;  /* White */
}

/* Check contrast for all states */
.button {
  color: #fff;
  background: #0070f3;  /* Check this */
}

.button:hover {
  background: #0051cc;  /* And this */
}

.button:disabled {
  background: #ccc;  /* And this */
}

Don't Rely on Color Alone

// āŒ Bad: Color only
<span style={{ color: 'red' }}>Error: Invalid email</span>

// āœ… Good: Icon + color + text
<span style={{ color: 'red' }}>
  <ErrorIcon /> Error: Invalid email
</span>

// Form validation
<input 
  aria-invalid="true"
  aria-describedby="email-error"
/>
<span id="email-error" role="alert">
  Please enter a valid email address
</span>

ARIA Attributes

When to Use ARIA

First Rule: Use semantic HTML instead of ARIA when possible.

<!-- āœ… Best: Semantic HTML -->
<button>Click me</button>

<!-- āš ļø OK but unnecessary -->
<div role="button" tabindex="0">Click me</div>

Common ARIA Attributes

<!-- Labels -->
<button aria-label="Close dialog">Ɨ</button>
<input aria-labelledby="email-label">
<span id="email-label">Email</span>

<!-- Descriptions -->
<input 
  aria-describedby="password-hint"
  type="password"
>
<span id="password-hint">
  Must be at least 8 characters
</span>

<!-- States -->
<button aria-pressed="true">Bold</button>
<div aria-expanded="false">Collapsed</div>
<input aria-invalid="true">
<div aria-hidden="true">Hidden from screen readers</div>

<!-- Live regions -->
<div role="alert">Form submitted successfully!</div>
<div aria-live="polite">Loading...</div>

<!-- Roles -->
<div role="navigation">...</div>
<div role="dialog" aria-modal="true">...</div>
<div role="tablist">
  <button role="tab" aria-selected="true">Tab 1</button>
  <button role="tab" aria-selected="false">Tab 2</button>
</div>

React Example

function Accordion({ items }) {
  const [expanded, setExpanded] = useState(null);
  
  return (
    <div>
      {items.map((item, index) => (
        <div key={index}>
          <button
            aria-expanded={expanded === index}
            aria-controls={`panel-${index}`}
            onClick={() => setExpanded(expanded === index ? null : index)}
          >
            {item.title}
          </button>
          <div
            id={`panel-${index}`}
            role="region"
            aria-labelledby={`button-${index}`}
            hidden={expanded !== index}
          >
            {item.content}
          </div>
        </div>
      ))}
    </div>
  );
}

Testing & Tools

Browser DevTools

Chrome DevTools:

1. Lighthouse audit (Accessibility section)

2. Inspect > Accessibility tree

3. Check color contrast

Firefox:

Accessibility Inspector

Browser Extensions

•axe DevTools: Automated testing
•WAVE: Visual feedback
•ARIA DevTools: ARIA attribute inspector

Screen Readers

•Mac: VoiceOver (Cmd+F5)
•Windows: NVDA (free), JAWS
•Mobile: TalkBack (Android), VoiceOver (iOS)

Automated Testing

// Jest + Testing Library
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

test('should have no a11y violations', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Manual Testing Checklist

āœ… Tab through all interactive elements

āœ… Use screen reader to navigate

āœ… Check color contrast

āœ… Test with keyboard only (no mouse)

āœ… Zoom to 200% - still usable?

āœ… Test forms with validation errors