Why Web Accessibility Matters
Web accessibility (a11y — the 11 letters between “a” and “y” in “accessibility”) is about making the web equally usable for people with disabilities. If a building only has stairs, wheelchair users cannot enter. Accessibility serves as the ramp for the web.
Approximately 15% of the world’s population (1 billion people) has some form of disability. Accessibility is a moral obligation, a legal requirement, and a business opportunity.
The Four WCAG Principles (POUR)
| Principle | Meaning | Example |
|---|---|---|
| Perceivable | All content must be perceivable | Providing alt text for images |
| Operable | All functionality must be keyboard-accessible | Tab navigation, Enter activation |
| Understandable | Content and UI must be understandable | Clear error messages, consistent UI |
| Robust | Accessible via diverse technologies | Screen readers, voice recognition, etc. |
Semantic HTML: The Foundation of Accessibility
Semantic HTML accounts for 80% of accessibility. Use the correct HTML elements before resorting to ARIA.
<!-- Wrong: building everything with divs (screen readers cannot determine structure) -->
<div class="header">
<div class="nav">
<div class="link" onclick="navigate('/')">Home</div>
<div class="link" onclick="navigate('/about')">About</div>
</div>
</div>
<div class="main">
<div class="article">
<div class="title">Web Accessibility Guide</div>
<div class="content">...</div>
</div>
</div>
<!-- Correct: using semantic elements (automatically conveys roles and structure) -->
<header>
<nav aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
<main>
<article>
<h1>Web Accessibility Guide</h1>
<p>...</p>
</article>
</main>
<footer>
<p>Copyright 2026</p>
</footer>
Semantic Elements to ARIA Role Mapping
| HTML Element | Implicit ARIA Role | Screen Reader Reads |
|---|---|---|
nav | navigation | ”navigation” |
main | main | ”main content” |
header | banner | ”banner” |
footer | contentinfo | ”content info” |
button | button | ”button” |
a[href] | link | ”link” |
h1~h6 | heading | ”heading level N” |
ul/ol | list | ”list, N items” |
ARIA Attributes in Practice
ARIA (Accessible Rich Internet Applications) adds accessibility information to complex UIs that cannot be expressed with semantic HTML alone.
Core Rule: Prefer Semantic HTML Over ARIA
<!-- Wrong: adding ARIA role to a div -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>
<!-- Correct: using a native button -->
<button onclick="submit()">Submit</button>
<!-- button automatically provides role="button", keyboard support, and focus management -->
Common ARIA Patterns
<!-- 1. Accordion -->
<button
aria-expanded="false"
aria-controls="panel-1"
id="accordion-1">
Frequently Asked Questions
</button>
<div
id="panel-1"
role="region"
aria-labelledby="accordion-1"
hidden>
<p>Accessibility is for all users.</p>
</div>
<!-- 2. Tab Panel -->
<div role="tablist" aria-label="Content tabs">
<button role="tab" aria-selected="true"
aria-controls="tab-panel-1" id="tab-1">
Overview
</button>
<button role="tab" aria-selected="false"
aria-controls="tab-panel-2" id="tab-2"
tabindex="-1">
Details
</button>
</div>
<div role="tabpanel" id="tab-panel-1"
aria-labelledby="tab-1">
<p>Overview content goes here.</p>
</div>
<div role="tabpanel" id="tab-panel-2"
aria-labelledby="tab-2" hidden>
<p>Detailed content goes here.</p>
</div>
<!-- 3. Live Region (screen reader announces immediately) -->
<div aria-live="polite" aria-atomic="true" id="status">
<!-- Dynamic messages inserted via JavaScript -->
</div>
<!-- 4. Modal Dialog -->
<dialog aria-labelledby="modal-title" aria-modal="true">
<h2 id="modal-title">Confirm Deletion</h2>
<p>Are you sure you want to delete this?</p>
<button>Confirm</button>
<button>Cancel</button>
</dialog>
Keyboard Navigation
All interactive elements must be keyboard-accessible.
// Tab panel keyboard navigation implementation
document.querySelector("[role='tablist']").addEventListener("keydown", (e) => {
const tabs = [...e.currentTarget.querySelectorAll("[role='tab']")];
const currentIndex = tabs.indexOf(e.target);
let newIndex;
switch (e.key) {
case "ArrowRight": // Right arrow → next tab
newIndex = (currentIndex + 1) % tabs.length;
break;
case "ArrowLeft": // Left arrow → previous tab
newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
break;
case "Home": // Home → first tab
newIndex = 0;
break;
case "End": // End → last tab
newIndex = tabs.length - 1;
break;
default:
return; // Ignore other keys
}
// Move focus and activate tab
tabs[newIndex].focus();
tabs[newIndex].click();
e.preventDefault();
});
/* Focus styles — visual indicator for keyboard users */
/* Wrong: removing focus styles */
*:focus { outline: none; } /* Never do this! */
/* Correct: clear focus indicator */
:focus-visible {
outline: 3px solid #4f46e5; /* Indigo outline */
outline-offset: 2px; /* 2px offset from element */
border-radius: 4px;
}
/* Hide outline on mouse click, show only during keyboard navigation */
:focus:not(:focus-visible) {
outline: none;
}
/* Skip link — lets keyboard users skip repetitive navigation */
.skip-link {
position: absolute;
top: -100%;
left: 0;
z-index: 9999;
padding: 1rem;
background: #1e1b4b;
color: white;
}
.skip-link:focus {
top: 0; /* Shown on screen when focused */
}
<!-- Skip link usage -->
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header>...</header>
<nav>...</nav>
<main id="main-content">
<!-- Main content -->
</main>
</body>
Images and Alternative Text
<!-- Informational image — provide a specific description -->
<img src="chart.png" alt="Quarterly revenue for 2025 — Q1 $10M, Q2 $15M, Q3 $20M" />
<!-- Decorative image — empty alt (screen readers skip it) -->
<img src="decoration.svg" alt="" />
<!-- Image inside a link — describe the link's purpose -->
<a href="/profile">
<img src="avatar.jpg" alt="Go to my profile" />
</a>
<!-- Complex image — link to a detailed description -->
<figure>
<img src="architecture.png"
alt="Microservices architecture diagram"
aria-describedby="arch-desc" />
<figcaption id="arch-desc">
The API Gateway routes requests to three services (Auth, Product, Order),
each using an independent database.
</figcaption>
</figure>
Form Accessibility
<!-- Properly structured form -->
<form aria-labelledby="form-title">
<h2 id="form-title">Sign Up</h2>
<!-- Label and input association is required -->
<div>
<label for="user-email">Email (required)</label>
<input
type="email"
id="user-email"
name="email"
required
aria-required="true"
aria-describedby="email-hint email-error"
autocomplete="email" />
<p id="email-hint">e.g., user@example.com</p>
<p id="email-error" role="alert" hidden>
Please enter a valid email address.
</p>
</div>
<!-- Password — requirement instructions -->
<div>
<label for="user-pw">Password (required)</label>
<input
type="password"
id="user-pw"
name="password"
required
aria-required="true"
aria-describedby="pw-requirements"
minlength="8"
autocomplete="new-password" />
<ul id="pw-requirements">
<li>At least 8 characters</li>
<li>Must include letters, numbers, and special characters</li>
</ul>
</div>
<button type="submit">Sign Up</button>
</form>
Accessibility Testing Tools
| Tool | Type | Purpose |
|---|---|---|
| axe DevTools | Browser extension | Automated accessibility testing |
| Lighthouse | Built into Chrome | Accessibility score measurement |
| NVDA | Screen reader (Windows) | Real screen reader testing |
| VoiceOver | Screen reader (macOS) | Real screen reader testing |
| Colour Contrast Analyser | Desktop app | Color contrast verification |
# Automated testing with axe-core CLI
npx @axe-core/cli https://example.com
# Integrate accessibility testing into component tests with jest-axe
npm install --save-dev jest-axe
Practical Tips
- Use semantic HTML first: Most accessibility issues can be resolved simply by using the correct HTML elements. Avoid overusing
divandspan. - Navigate your site using only the keyboard: Test all functionality with Tab, Shift+Tab, Enter, Space, Escape, and arrow keys.
- Never remove
:focus-visiblestyles: Removing focus indicators leaves keyboard users unable to see where they are on the page. - Do not rely on color alone: If errors are indicated only by red color, users with color vision deficiencies cannot perceive them. Always provide icons or text alongside color.
- ARIA is a last resort: Incorrect ARIA usage actually worsens accessibility. Remember the rule: “No ARIA is better than bad ARIA.”
- Test with real screen readers: NVDA (free) on Windows and VoiceOver (built-in) on macOS provide the most reliable way to verify accessibility by listening directly.