Web Accessibility Fundamentals: Building for Everyone
Accessibility isn't optional - it's essential for inclusive products and legal compliance.
Quick Navigation: Semantic HTML ⢠Alternative Text & Labels ⢠Keyboard Navigation ⢠Color Contrast & Visual Design ⢠ARIA Attributes ⢠Testing & Tools
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
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>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:1Examples
/* ā 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
Screen Readers
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