Web accessibility ensures everyone can use your site, regardless of ability. About 15% of the world's population has some form of disability. Beyond ethics and inclusion, accessibility improves SEO, reaches more users, and is often legally required. These fundamentals cover most common issues.
Semantic HTML
The foundation of accessibility is using HTML elements for their intended purpose:
| Don't use | Use instead | Why |
|---|---|---|
<div onclick> |
<button> |
Keyboard accessible, announced as button |
<div> for navigation |
<nav> |
Screen readers identify it as navigation |
<span> for links |
<a href> |
Keyboard accessible, focusable |
<b> |
<strong> |
Conveys importance, not just visual |
<i> |
<em> |
Conveys emphasis |
Document structure
<body>
<header>
<nav><!-- Main navigation --></nav>
</header>
<main>
<h1>Page Title</h1>
<article>
<h2>Section</h2>
<p>Content...</p>
</article>
</main>
<aside><!-- Related content --></aside>
<footer><!-- Footer content --></footer>
</body>
Heading hierarchy
Use headings in order—don't skip levels:
<!-- Good -->
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
<!-- Bad -->
<h1>Page Title</h1>
<h3>Skipped h2!</h3>
Keyboard navigation
Everything must work with keyboard alone:
| Key | Action |
|---|---|
| Tab | Move to next focusable element |
| Shift+Tab | Move to previous element |
| Enter | Activate buttons, links |
| Space | Activate buttons, toggle checkboxes |
| Escape | Close modals, menus |
| Arrow keys | Navigate within components |
Focus visibility
Never remove focus outlines without replacement:
/* Bad */
button:focus {
outline: none;
}
/* Good */
button:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
}
Focus management
Move focus appropriately:
// After opening modal
modalRef.current?.focus();
// After closing modal
triggerButtonRef.current?.focus();
Skip links
Help keyboard users bypass navigation:
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<main id="main-content" tabindex="-1">
<!-- Page content -->
</main>
.skip-link {
position: absolute;
left: -9999px;
}
.skip-link:focus {
position: static;
left: auto;
}
Color and contrast
Contrast ratios
| WCAG Level | Normal text | Large text |
|---|---|---|
| AA | 4.5:1 | 3:1 |
| AAA | 7:1 | 4.5:1 |
Large text is 18pt (24px) or 14pt (18.5px) bold.
Use a contrast checker to verify.
Don't rely on color alone
<!-- Bad: only color indicates error -->
<input style="border-color: red;" />
<!-- Good: color + icon + text -->
<input aria-invalid="true" aria-describedby="error-msg" />
<span id="error-msg">
<svg aria-hidden="true"><!-- Error icon --></svg>
Please enter a valid email
</span>
Images and media
Alt text guidelines
| Image type | Alt text |
|---|---|
| Informative | Describe content and purpose |
| Decorative | alt="" (empty) |
| Functional (button/link) | Describe the action |
| Complex (chart/graph) | Brief alt + detailed description |
<!-- Informative -->
<img src="chart.png" alt="Sales increased 25% in Q4 2025" />
<!-- Decorative -->
<img src="decoration.png" alt="" />
<!-- Functional -->
<button>
<img src="search.svg" alt="Search" />
</button>
<!-- Complex with description -->
<figure>
<img src="chart.png" alt="Q4 sales chart" aria-describedby="chart-desc" />
<figcaption id="chart-desc">
Detailed breakdown: January $100k, February $120k...
</figcaption>
</figure>
Video and audio
| Content | Required |
|---|---|
| Video | Captions |
| Audio | Transcript |
| Complex video | Audio description |
Forms
Labels
Every input needs a label:
<!-- Explicit label -->
<label for="email">Email address</label>
<input type="email" id="email" name="email" />
<!-- Implicit label -->
<label>
Email address
<input type="email" name="email" />
</label>
<!-- Visually hidden label -->
<label for="search" class="sr-only">Search</label>
<input type="search" id="search" placeholder="Search..." />
Error messages
Connect errors to inputs:
<label for="email">Email</label>
<input
type="email"
id="email"
aria-invalid="true"
aria-describedby="email-error"
/>
<span id="email-error" role="alert">
Please enter a valid email address
</span>
Required fields
<label for="name">
Name <span aria-hidden="true">*</span>
<span class="sr-only">(required)</span>
</label>
<input type="text" id="name" required aria-required="true" />
ARIA basics
ARIA supplements HTML when semantic elements aren't enough:
| Attribute | Purpose |
|---|---|
aria-label |
Provide accessible name |
aria-labelledby |
Reference visible label |
aria-describedby |
Reference description |
aria-hidden |
Hide from assistive tech |
aria-live |
Announce dynamic updates |
role |
Define element purpose |
<!-- Icon button -->
<button aria-label="Close menu">
<svg aria-hidden="true"><!-- X icon --></svg>
</button>
<!-- Live region for updates -->
<div aria-live="polite" aria-atomic="true">
3 items in cart
</div>
First rule of ARIA
Don't use ARIA if native HTML works:
<!-- Bad -->
<div role="button" tabindex="0" onclick="...">Click me</div>
<!-- Good -->
<button onclick="...">Click me</button>
Testing accessibility
| Tool | Purpose |
|---|---|
| axe DevTools | Automated testing |
| WAVE | Visual feedback |
| Lighthouse | Audit with scoring |
| Screen reader | Manual testing |
| Keyboard | Navigation testing |
Manual testing checklist
- Navigate with Tab only
- Verify focus is visible
- Test with screen reader (NVDA, VoiceOver)
- Check color contrast
- Zoom to 200%
- Verify heading structure
Summary
Use semantic HTML as the foundation. Ensure keyboard navigation works everywhere with visible focus. Meet contrast ratios (4.5:1 for text). Provide meaningful alt text for images. Label all form inputs and connect error messages. Use ARIA sparingly when HTML isn't enough. Test with keyboard, screen readers, and automated tools.
