Web accessibility ensures that people with disabilities can use your website. This guide covers everything you need to build accessible applications.
Semantic HTML
Structure Elements
<!-- Header -->
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
</header>
<!-- Main content -->
<main>
<article>
<h1>Article Title</h1>
<p>Article content...</p>
</article>
<aside>
<h2>Related Articles</h2>
</aside>
</main>
<!-- Footer -->
<footer>
<p>© 2026 Company</p>
</footer>
Headings
<!-- Correct heading hierarchy -->
<h1>Page Title</h1>
<h2>Section 1</h2>
<h3>Subsection 1.1</h3>
<h3>Subsection 1.2</h3>
<h2>Section 2</h2>
<h3>Subsection 2.1</h3>
<!-- DON'T skip levels -->
<!-- Bad -->
<h1>Title</h1>
<h3>Subtitle</h3>
<!-- Good -->
<h1>Title</h1>
<h2>Subtitle</h2>
Buttons vs Links
<!-- Button - performs action -->
<button onclick="submitForm()">Submit</button>
<button onclick="openModal()">Open</button>
<button onclick="toggleMenu()">Menu</button>
<!-- Link - navigates -->
<a href="/page">Go to page</a>
<a href="/download">Download file</a>
<a href="#section">Jump to section</a>
ARIA Attributes
Role Attributes
<!-- Navigation role -->
<nav role="navigation" aria-label="Main">
<!-- Banner role (header) -->
<header role="banner">
<!-- Main role -->
<main role="main">
<!-- Contentinfo role (footer) -->
<footer role="contentinfo">
<!-- Complementary role -->
<aside role="complementary">
<!-- Search role -->
<form role="search">
ARIA Labels
<!-- aria-label -->
<button aria-label="Close menu">
<svg>...</svg>
</button>
<!-- aria-labelledby -->
<div aria-labelledby="modal-title">
<h2 id="modal-title">Confirm Action</h2>
</div>
<!-- aria-describedby -->
<input
type="email"
aria-describedby="email-help"
>
<span id="email-help">We'll never share your email</span>
ARIA States
<!-- Expanded -->
<button aria-expanded="false" aria-controls="menu">
Menu
</button>
<ul id="menu" hidden>
<li>Item 1</li>
</ul>
<!-- Selected -->
<tablist>
<tab aria-selected="true">Tab 1</tab>
<tab aria-selected="false">Tab 2</tab>
</tablist>
<!-- Checked/Radio -->
<input type="radio" aria-checked="true">
<input type="checkbox" aria-checked="true">
<!-- Disabled -->
<button disabled aria-disabled="true">
Live Regions
<!-- Polite - announced when idle -->
<div aria-live="polite" aria-atomic="true">
<p>Changes saved</p>
</div>
<!-- Assertive - announced immediately -->
<div role="alert" aria-live="assertive">
<p>Error: Please correct the form</p>
</div>
<!-- Status -->
<div role="status" aria-live="polite">
<p>Ready</p>
</div>
Keyboard Navigation
Focus Management
/* Visible focus indicator */
:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
/* Remove default focus (bad practice) */
/* DON'T do this */
/*:focus {
outline: none;
}*/
Tab Order
<!-- Logical tab order -->
<input tabindex="1">
<input tabindex="2">
<input tabindex="3">
<!-- Avoid positive tabindex -->
<!-- DON'T -->
<input tabindex="5">
<input tabindex="1">
<input tabindex="10">
<!-- Use 0 for natural order -->
<input tabindex="0"> <!-- included in natural order -->
<input tabindex="-1"> <!-- programmatically focusable only -->
Skip Links
<!-- Skip to main content -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: blue;
color: white;
padding: 8px;
z-index: 100;
}
.skip-link:focus {
top: 0;
}
</style>
<main id="main-content">
Keyboard Handlers
// Handle keyboard events
function handleKeyDown(event) {
switch (event.key) {
case 'Enter':
case ' ':
// Activate button
event.preventDefault();
selectItem();
break;
case 'ArrowDown':
event.preventDefault();
moveFocus(1);
break;
case 'ArrowUp':
event.preventDefault();
moveFocus(-1);
break;
case 'Escape':
closeModal();
break;
case 'Home':
event.preventDefault();
focusFirstItem();
break;
case 'End':
event.preventDefault();
focusLastItem();
break;
}
}
Forms
Labels
<!-- Explicit label -->
<label for="email">Email</label>
<input id="email" type="email">
<!-- Implicit label (wrapped) -->
<label>
Email
<input type="email">
</label>
<!-- Best - both -->
<label for="email">
Email
<span class="sr-only">(required)</span>
</label>
<input id="email" type="email" required aria-required="true">
Error Handling
<!-- Associate error with input -->
<label for="password">Password</label>
<input
id="password"
type="password"
aria-invalid="true"
aria-describedby="password-error"
>
<span id="password-error" role="alert">
Password must be at least 8 characters
</span>
<!-- Success state -->
<input
type="email"
aria-invalid="false"
aria-describedby="email-success"
>
<span id="email-success" role="status">
โ Valid email
</span>
Images
<!-- Decorative -->
<img src="decoration.png" alt="">
<!-- Informative -->
<img src="chart.png" alt="Sales chart showing 50% growth">
<!-- Complex - use long description -->
<img
src="chart.png"
alt="Sales trend"
aria-describedby="chart-desc"
>
<div id="chart-desc" hidden>
<!-- Long description -->
</div>
Testing
Automated Testing
// eslint-plugin-jsx-a11y
// .eslintrc
{
"plugins": ["jsx-a11y"],
"rules": {
"jsx-a11y/alt-text": "error",
"jsx-a11y/aria-props": "error",
"jsx-a11y/heading-has-content": "error",
"jsx-a11y/label-has-associated-control": "error"
}
}
// Axe-core
import axe from 'axe-core';
axe.run(document).then(results => {
console.log(results.violations);
});
// In tests
import { toHaveNoViolations } from 'jest-axe';
expect(element).toHaveNoViolations();
Manual Testing Checklist
1. Keyboard Only
- Can you navigate all interactive elements?
- Can you activate all buttons?
- Can you close modals with Escape?
- Is focus visible everywhere?
2. Screen Reader
- Are all images described?
- Are forms properly labeled?
- Are error messages announced?
- Is reading order logical?
3. Zoom
- Does layout work at 200% zoom?
- Is no content cut off?
- Are scrollbars available?
4. Color Contrast
- Do text and backgrounds have 4.5:1 ratio?
- Are focus indicators visible?
- Are links distinguishable?
Browser DevTools
// Accessibility panel in DevTools
// Shows accessibility tree
// Highlights accessibility issues
// Lighthouse accessibility audit
// Run in Chrome DevTools > Lighthouse
// Check: "Accessibility"
WCAG Guidelines
Level A (Minimum)
- Non-text content has alt text
- Info and relationships are programmatically determined
- Content can be presented in different ways
- Info and user components must be distinguishable
- Keyboard accessible
- No keyboard traps
- Page titles are descriptive
- Focus order is logical
Level AA (Standard)
- Contrast ratio 4.5:1 for text
- Text resizable to 200%
- Multiple ways to find pages
- Focus visible
- Consistent navigation
- Consistent identification
Level AAA (Enhanced)
- Contrast ratio 7:1 for text
- No timing for reading
- No content that could cause seizures
- Multiple ways to locate pages
- Focus not obscured
Color & Contrast
/* Minimum contrast (AA - 4.5:1) */
.text-primary {
color: #1a1a1a; /* on white */
background: #ffffff;
}
/* Enhanced contrast (AAA - 7:1) */
.text-enhanced {
color: #0d0d0d; /* on white */
background: #ffffff;
}
/* Large text (3:1) */
.text-large {
font-size: 18px; /* or 14px bold */
color: #4a4a4a;
background: #ffffff;
}
/* Focus indicators */
button:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
Screen Reader Testing
Common Screen Readers
- NVDA (Windows, free)
- JAWS (Windows, paid)
- VoiceOver (Mac/iOS, built-in)
- TalkBack (Android, built-in)
What to Test
- Reading order - Logical flow when navigating
- Alt text - Meaningful descriptions
- Form labels - Properly associated
- Headings - Correct hierarchy
- Links - Descriptive text
- Dynamic content - Announced with live regions
Summary
Key accessibility principles:
-
Semantic HTML
- Use proper elements
- Correct heading hierarchy
- Buttons vs links
-
ARIA
- Use when needed
- Don’t over-use
- Follow spec
-
Keyboard
- All functionality accessible
- Visible focus
- Skip links
-
Forms
- Labels always present
- Error association
- Clear instructions
-
Testing
- Automated tools
- Manual testing
- Real screen readers
Accessibility benefits everyone - implement from the start!
Comments