Web Accessibility (a11y) Complete Guide - Build Inclusive Websites
Learn web accessibility from scratch. Discover how to make your websites usable by everyone, including people with disabilities. Master ARIA, semantic HTML, keyboard navigation, and WCAG guidelines.
When we talk about building great websites, we often focus on stunning visuals, blazing-fast performance, and sleek animations. But there’s a crucial aspect that too many developers overlook: making sure everyone can use your website, regardless of their abilities or the devices they use.
That’s where web accessibility comes in — and no, it’s not just a nice-to-have feature. It’s a fundamental part of professional web development.

What Is Web Accessibility?
Web accessibility (often abbreviated as a11y — eleven letters between “a” and “y”) means creating websites and web applications that people with disabilities can use effectively.
But here’s the thing: accessibility isn’t just about disability. It’s about inclusive design — building experiences that work for everyone, in every situation.
Who Benefits from Accessibility?
People with disabilities represent approximately 15% of the global population — that’s over 1 billion people. Here are some common types of impairments:
| Type | Examples |
|---|---|
| Visual | Blindness, low vision, color blindness, light sensitivity |
| Auditory | Deafness, hard of hearing, auditory processing disorders |
| Motor | Limited dexterity, tremors, paralysis, missing limbs |
| Cognitive | Dyslexia, ADHD, autism, memory impairments |
But accessibility also helps:
- People using mobile devices in bright sunlight
- Users with slow or unstable internet connections
- Older users whose abilities change with age
- Anyone in a distracting environment (like a noisy subway)
The Four Principles of WCAG
The Web Content Accessibility Guidelines (WCAG), published by the World Wide Web Consortium (W3C), define the gold standard for web accessibility. They’re built on four core principles:
1. Perceivable
Information and user interface components must be presentable to users in ways they can perceive.
<!-- ❌ Bad: Image without alternative text -->
<img src="chart.png">
<!-- ✅ Good: Descriptive alt text -->
<img src="chart.png" alt="Sales increased 40% in Q3 compared to Q2">
<!-- ✅ Also good: Decorative images get empty alt -->
<img src="divider.png" alt="">
Key practices:
- Provide alt text for all meaningful images
- Use captions and transcripts for videos
- Ensure content makes sense when styled differently (e.g., dark mode)
- Don’t rely solely on color to convey information
2. Operable
User interface components and navigation must be operable by all users.
<!-- ❌ Bad: Keyboard trap inside modal -->
<div class="modal" tabindex="-1">
<button onclick="closeModal()">Close</button>
<!-- Focus cannot escape this modal -->
</div>
<!-- ✅ Good: Modal with proper focus management -->
<div class="modal" role="dialog" aria-modal="true" tabindex="-1">
<button onclick="closeModal()" autofocus>Close</button>
<!-- Tab order is managed; Escape key closes modal -->
</div>
Key practices:
- Make everything work with keyboard navigation alone
- Provide enough time for users to read and interact
- Avoid content that flashes more than three times per second
- Offer clear navigation mechanisms (skip links, breadcrumbs)
3. Understandable
Information and the operation of the user interface must be understandable.
<!-- ❌ Bad: Unclear button label -->
<button>Submit</button>
<!-- ✅ Good: Clear, descriptive label -->
<button type="submit">Create New Project</button>
<!-- ❌ Bad: Error message doesn't explain the problem -->
<p>Error!</p>
<!-- ✅ Good: Specific error message with guidance -->
<p class="error">Email address is invalid. Please include @ and a domain name (e.g., user@example.com).</p>
Key practices:
- Use clear, simple language
- Make error messages specific and actionable
- Keep layouts consistent across pages
- Allow users to undo, confirm, or correct mistakes
4. Robust
Content must be robust enough to be interpreted reliably by a wide variety of user agents, including assistive technologies.
<!-- ❌ Bad: Non-standard custom element -->
<my-button onclick="handleClick()">Click Me</my-button>
<!-- ✅ Good: Standard HTML element -->
<button onclick="handleClick()">Click Me</button>
<!-- ❌ Bad: Div used as interactive element -->
<div onclick="toggleMenu()" style="cursor: pointer;">Menu</div>
<!-- ✅ Good: Semantic interactive element -->
<button aria-expanded="false" aria-controls="menu" onclick="toggleMenu()">Menu</button>
Key practices:
- Use valid, semantic HTML
- Ensure compatibility with screen readers and other assistive tech
- Test with multiple browsers and devices
- Follow web standards consistently
Essential HTML Techniques for Accessibility
The foundation of accessible web development is semantic HTML. When you use the right HTML elements, you get accessibility “for free.”
Proper Heading Structure
Headings create a document outline that screen reader users rely on to navigate content.
<!-- ✅ Correct heading hierarchy -->
<h1>Main Page Title</h1>
<h2>Section One</h2>
<h3>Subsection A</h3>
<h3>Subsection B</h3>
<h2>Section Two</h2>
<h3>Subsection C</h3>
<!-- ❌ Wrong: Skipping heading levels -->
<h1>Main Title</h1>
<h3>Skipped h2! Confusing for screen readers.</h3>
Landmark Regions
Landmarks help screen reader users jump to different sections of a page quickly.
<!-- ✅ Use semantic HTML5 landmark elements -->
<header>
<nav aria-label="Main navigation">
<!-- Navigation links -->
</nav>
</header>
<main>
<article>
<!-- Main content -->
</article>
</main>
<aside>
<!-- Sidebar content -->
</aside>
<footer>
<!-- Footer content -->
</footer>
Accessible Forms
Forms are often the most inaccessible part of a website. Here’s how to get it right:
<form action="/subscribe" method="POST">
<!-- ✅ Each input has a visible label -->
<div class="form-group">
<label for="email">Email Address</label>
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
aria-describedby="email-help"
>
<small id="email-help">We'll never share your email with anyone.</small>
</div>
<!-- ✅ Error messages linked to inputs -->
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
name="username"
required
aria-invalid="true"
aria-describedby="username-error"
>
<span id="username-error" class="error" role="alert">
Username must be at least 3 characters long.
</span>
</div>
<!-- ✅ Radio buttons grouped properly -->
<fieldset>
<legend>Preferred Contact Method</legend>
<label>
<input type="radio" name="contact" value="email"> Email
</label>
<label>
<input type="radio" name="contact" value="phone"> Phone
</label>
<label>
<input type="radio" name="contact" value="mail"> Postal Mail
</label>
</fieldset>
<button type="submit">Subscribe Now</button>
</form>
Links That Make Sense
Every link should clearly describe its destination or purpose.
<!-- ❌ Bad: Vague link text -->
<a href="/about">Click here</a>
<a href="/blog/post-123">Read more</a>
<!-- ✅ Good: Descriptive link text -->
<a href="/about">Learn more about Devs2.org</a>
<a href="/blog/post-123">Read the complete CSS Grid tutorial</a>
<!-- ✅ For icon-only links, provide accessible name -->
<a href="/search" aria-label="Search our articles">
<svg><!-- search icon --></svg>
</a>
Color and Visual Design
Color plays a huge role in web design, but relying on color alone excludes users with color blindness or those viewing screens in poor conditions.
Sufficient Color Contrast
The WCAG AA standard requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (18pt bold or 14pt regular).
/* ❌ Bad: Low contrast */
.text-low-contrast {
color: #aaaaaa; /* Light gray on white — fails WCAG */
}
/* ✅ Good: High contrast */
.text-high-contrast {
color: #333333; /* Dark gray on white — passes WCAG AA */
}
/* ✅ Large text needs less contrast */
.large-text {
color: #777777; /* Acceptable for large text (18pt+) */
}
Quick reference for minimum contrast ratios:
| Text Size | Minimum Ratio (AA) | Minimum Ratio (AAA) |
|---|---|---|
| Normal (< 18pt) | 4.5:1 | 7:1 |
| Large (≥ 18pt bold / ≥ 14pt regular) | 3:1 | 4.5:1 |
Don’t Rely on Color Alone
/* ❌ Bad: Only color indicates state */
.input-error {
border: 2px solid red;
}
/* ✅ Good: Color + icon + text */
.input-error {
border: 2px solid red;
background-image: url('error-icon.svg');
background-repeat: no-repeat;
background-position: left center;
padding-left: 2rem;
}
.input-error::after {
content: "Please fix this field";
display: block;
font-size: 0.875rem;
color: #dc2626;
}
Supporting Dark Mode and Reduced Motion
/* Respect user preferences */
@media (prefers-color-scheme: dark) {
body {
background-color: #1a1a1a;
color: #f0f0f0;
}
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
ARIA: When and How to Use It
ARIA (Accessible Rich Internet Applications) fills gaps when native HTML isn’t enough. But remember the First Rule of ARIA:
Don’t use ARIA if a native HTML element will do.
Common ARIA Attributes
<!-- Live regions: Announce dynamic updates to screen readers -->
<div aria-live="polite" aria-atomic="true">
<!-- This content will be announced when it changes -->
<span id="notification"></span>
</div>
<script>
// Screen readers will announce this update politely
document.getElementById('notification').textContent =
'Your form has been saved successfully.';
</script>
<!-- Expanded/collapsed state -->
<button
aria-expanded="false"
aria-controls="submenu"
onclick="toggleMenu()"
>
Settings
</button>
<div id="submenu" hidden>
<!-- Menu items -->
</div>
<!-- Loading states -->
<button disabled aria-busy="true">
Saving...
</button>
<!-- Skip links for keyboard users -->
<a href="#main-content" class="skip-link">Skip to main content</a>
ARIA Roles You Might Need
| Role | Purpose | Example |
|---|---|---|
role="dialog" | Modal dialog | Pop-up forms, confirmation boxes |
role="alert" | Urgent message | Error notifications |
role="tablist" | Tab container | Tabbed interfaces |
role="progressbar" | Progress indicator | Loading bars |
role="navigation" | Navigation section | Nav menus |
role="complementary" | Side content | Sidebars |
Keyboard Navigation
Many users navigate entirely with a keyboard. Your site must support this.
Focus Management
/* ✅ Visible focus indicators */
*:focus {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
/* ❌ Never remove focus without providing an alternative */
*:focus {
outline: none; /* BAD — removes keyboard visibility */
}
Skip Navigation Link
Help keyboard users bypass repetitive navigation:
<!-- Place this as the very first focusable element -->
<a href="#main-content" class="skip-link">
Skip to main content
</a>
<nav><!-- Navigation menu --></nav>
<main id="main-content">
<!-- Page content -->
</main>
<style>
.skip-link {
position: absolute;
top: -100%;
left: 0;
background: #2563eb;
color: white;
padding: 0.75rem 1.5rem;
z-index: 100;
font-weight: bold;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}
</style>
Interactive Elements Must Be Keyboard-Accessible
// ✅ Custom dropdown that works with keyboard
const dropdown = document.querySelector('.custom-dropdown');
const trigger = dropdown.querySelector('.trigger');
const menu = dropdown.querySelector('.menu');
trigger.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'Enter') {
e.preventDefault();
menu.hidden = false;
trigger.setAttribute('aria-expanded', 'true');
menu.querySelector('li').focus();
}
});
// Trap focus within modal dialogs
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
Testing Your Accessibility
You can’t make a website accessible without testing it. Here’s a practical testing checklist:
Automated Tools
| Tool | What It Does | Best For |
|---|---|---|
| axe DevTools | Browser extension for real-time analysis | Development workflow |
| Lighthouse | Built into Chrome DevTools | Performance + accessibility audit |
| WAVE | Web-based visual feedback | Quick checks on any page |
| Pa11y | Command-line automated testing | CI/CD integration |
Manual Testing Checklist
□ Navigate the entire site using ONLY the keyboard (Tab, Shift+Tab, Enter, Space, Escape)
□ Check that focus order follows the visual layout
□ Verify all images have appropriate alt text
□ Confirm heading hierarchy is logical (no skipped levels)
□ Test all forms with labels, error messages, and validation
□ Check color contrast ratios meet WCAG AA standards
□ Verify links have descriptive text
□ Test with a screen reader (NVDA on Windows, VoiceOver on Mac/iOS)
□ Resize browser text up to 200% — does content remain readable?
□ Turn off all styling — is the content still understandable?
Screen Reader Testing
Windows: Download NVDA (free, open-source) macOS/iOS: Enable VoiceOver (Cmd+F5 on Mac, toggle in Settings on iOS) Android: Enable TalkBack (in Accessibility settings)
Pro tip: Try browsing your own website with a screen reader turned on. You’ll quickly discover issues you’d never notice visually.
Accessibility in Popular Frameworks
React
// ✅ Accessible button component
function AccessibleButton({ onClick, children }) {
return (
<button
onClick={onClick}
aria-label="Custom action"
className="btn btn-primary"
>
{children}
</button>
);
}
// ✅ Accessible modal with focus trapping
function Modal({ isOpen, onClose, title, children }) {
const modalRef = useRef(null);
useEffect(() => {
if (isOpen && modalRef.current) {
modalRef.current.focus();
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
onKeyDown={(e) => e.key === 'Escape' && onClose()}
>
<h2 id="modal-title">{title}</h2>
<div ref={modalRef} tabIndex={-1}>
{children}
</div>
<button onClick={onClose} aria-label="Close dialog">✕</button>
</div>
);
}
// ✅ Accessible accordion
function AccordionItem({ title, children, isOpen }) {
return (
<div className="accordion-item">
<h3>
<button
aria-expanded={isOpen}
aria-controls={`panel-${title}`}
onClick={() => {/* toggle logic */}}
>
{title}
</button>
</h3>
<div
id={`panel-${title}`}
role="region"
hidden={!isOpen}
>
{children}
</div>
</div>
);
}
Astro
---
// ✅ Semantic HTML in Astro templates
---
<article>
<header>
<h1>{frontmatter.title}</h1>
<time datetime={frontmatter.publishedDate}>
{new Date(frontmatter.publishedDate).toLocaleDateString('vi-VN')}
</time>
</header>
<main>
<slot />
</main>
<footer>
<p>Written by {frontmatter.author}</p>
</footer>
</article>
<!-- ✅ Skip link -->
<a href="#main-content" class="skip-link">Skip to content</a>
Common Accessibility Mistakes (And How to Fix Them)
Mistake 1: Using <div> and <span> for Everything
<!-- ❌ Bad -->
<div onclick="navigate('/home')">Home</div>
<span class="link" onclick="openModal()">More Info</span>
<!-- ✅ Good -->
<a href="/home">Home</a>
<button onclick="openModal()">More Info</button>
Mistake 2: Auto-playing Media
<!-- ❌ Bad: Audio auto-plays -->
<audio autoplay>
<source src="intro.mp3" type="audio/mpeg">
</audio>
<!-- ✅ Good: User controls playback -->
<audio controls preload="metadata">
<source src="intro.mp3" type="audio/mpeg">
</audio>
<!-- ✅ Even better: Muted by default -->
<video muted playsinline controls>
<source src="demo.mp4" type="video/mp4">
</video>
Mistake 3: Missing Form Labels
<!-- ❌ Bad: No visible label -->
<input type="text" placeholder="Enter your name">
<!-- ✅ Good: Visible label -->
<label for="name">Full Name</label>
<input type="text" id="name" placeholder="Enter your name">
<!-- ✅ Alternative: Visually hidden label -->
<label for="search" class="sr-only">Search</label>
<input type="search" id="search" placeholder="Search...">
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
Mistake 4: Ignoring Focus States
/* ❌ Bad: Invisible focus */
button:focus {
outline: none;
}
/* ✅ Good: Clear, visible focus */
button:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.2);
}
The Business Case for Accessibility
Beyond ethics and law, accessibility makes business sense:
- Market reach: 1 billion+ potential users with disabilities worldwide
- SEO boost: Accessible sites tend to rank higher (search engines love semantic HTML)
- Legal compliance: Laws like Section 508, ADA, and EN 301 549 require accessibility
- Better UX: Accessible features benefit all users (e.g., captions help in noisy environments)
- Reduced maintenance: Clean, semantic code is easier to maintain and extend
Getting Started: Your Accessibility Action Plan
Ready to make your website more accessible? Here’s a step-by-step plan:
Phase 1: Quick Wins (1-2 days)
- Add alt text to all images
- Ensure all links have descriptive text
- Check heading hierarchy — no skipped levels
- Add labels to all form inputs
- Verify color contrast meets WCAG AA
Phase 2: Core Improvements (1-2 weeks)
- Implement keyboard navigation throughout
- Add skip navigation links
- Ensure focus indicators are visible
- Test with a screen reader
- Add ARIA attributes where needed
Phase 3: Advanced (Ongoing)
- Conduct user testing with people who have disabilities
- Integrate automated accessibility testing into CI/CD
- Create an accessibility statement for your site
- Train your team on accessible development practices
- Regularly audit and remediate new issues
Conclusion
Web accessibility isn’t a checkbox to tick or a project to finish. It’s a mindset — a commitment to building technology that works for everyone.
The good news? You don’t need to be perfect to make a difference. Start with the basics: use semantic HTML, add alt text, ensure keyboard navigation, and check your color contrast. Every small improvement makes the web a little more inclusive.
Remember: the best accessibility feature is a well-designed, thoughtfully built website. When you prioritize accessibility from the start — not as an afterthought — you create better products for everyone.
As the saying goes in the accessibility community:
“Nothing about us without us.”
Involve people with disabilities in your design and testing process, listen to their feedback, and keep learning. The web is better when everyone can participate.
Resources for Further Learning: