HTML Accessibility (a11y)
Accessibility means building for everyone - including users with visual, motor, auditory, and cognitive disabilities. Most a11y issues are fixable with correct HTML; ARIA is the last resort, not the first tool.
ARIA Roles & Attributes
ARIA (Accessible Rich Internet Applications) fills gaps where HTML semantics are insufficient. The first rule of ARIA: don't use ARIA - use native HTML elements instead whenever possible.
<!-- Role: overrides or provides semantic meaning -->
<div role="button" tabindex="0">Custom Button</div> <!-- Prefer <button> -->
<div role="alert">Error: Form submission failed</div> <!-- Live region, announced immediately -->
<div role="status">Changes saved</div> <!-- Polite live region -->
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm Delete</h2>
<p>Are you sure you want to delete this item?</p>
<button>Cancel</button>
<button>Delete</button>
</div>
<!-- aria-label: names an element without visible text -->
<button aria-label="Close dialog">
<svg aria-hidden="true"><use href="#icon-x"/></svg>
</button>
<nav aria-label="Breadcrumb">...</nav> <!-- Distinguish multiple navs -->
<nav aria-label="Pagination">...</nav>
<!-- aria-labelledby: points to existing visible text -->
<section aria-labelledby="products-heading">
<h2 id="products-heading">Featured Products</h2>
</section>
<!-- aria-describedby: provides additional description -->
<input type="password" aria-describedby="pwd-hint">
<span id="pwd-hint">Min 8 characters, include a number</span>
<!-- State attributes -->
<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>...</ul>
<input type="checkbox" aria-checked="mixed"> <!-- Indeterminate state -->
<li role="option" aria-selected="true">Option 1</li>
<button aria-pressed="true">Bold</button> <!-- Toggle button -->
<span aria-live="polite" aria-atomic="true">Loading...</span>
<!-- aria-hidden: hide decorative elements from screen readers -->
<span aria-hidden="true">★★★☆☆</span>
<span class="sr-only">3 out of 5 stars</span> <!-- Visible only to screen readers -->
<!-- Disable an element for AT -->
<div aria-disabled="true">Unavailable option</div>Skip Links & Keyboard Navigation
<!-- Skip link: first focusable element, bypasses repetitive nav -->
<!-- Visible on focus, hidden otherwise -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<header>...</header>
<main id="main-content" tabindex="-1">...</main>
<!-- CSS for skip link (show on focus only) -->
<style>
.skip-link {
position: absolute;
top: -100%;
left: 0;
background: #000;
color: #fff;
padding: 8px 16px;
z-index: 9999;
text-decoration: none;
}
.skip-link:focus {
top: 0;
}
</style>
<!-- tabindex -->
<div tabindex="0">Focusable div (avoid - use <button> or <a>)</div>
<div tabindex="-1">Programmatically focusable, not in tab order</div>
<!-- tabindex="1+" disrupts natural order - AVOID -->
<!-- Focus management in modals -->
<script>
function openModal(modalEl) {
modalEl.removeAttribute('hidden');
// Move focus into modal
const firstFocusable = modalEl.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
// Trap focus inside modal
modalEl.addEventListener('keydown', trapFocus);
}
function closeModal(modalEl, triggerEl) {
modalEl.setAttribute('hidden', '');
// Return focus to the element that opened the modal
triggerEl?.focus();
}
</script>Images, Color & Screen Readers
<!-- Alt text guidelines -->
<img src="hero.jpg" alt="Developer typing on a mechanical keyboard at night">
<!-- Informative image: describe what it shows, why it's there -->
<img src="logo.svg" alt="Acme Corp">
<!-- Logo: company name only -->
<img src="decorative-border.png" alt="">
<!-- Decorative: empty alt, screen reader skips it -->
<img src="chart.png"
alt="Bar chart showing 40% increase in users from Q1 to Q2 2025"
aria-describedby="chart-details">
<p id="chart-details">Full data table: Q1: 1,200 users. Q2: 1,680 users. Growth: 480 (40%).</p>
<!-- Complex image: short alt + long description -->
<!-- Icon buttons: always label the action -->
<button aria-label="Delete item">
<svg aria-hidden="true" focusable="false">
<use href="#trash-icon"></use>
</svg>
</button>
<!-- Color contrast requirements (WCAG 2.1 AA) -->
<!-- Normal text (< 18pt): 4.5:1 ratio -->
<!-- Large text (>= 18pt or 14pt bold): 3:1 ratio -->
<!-- UI components, icons: 3:1 ratio against adjacent colors -->
<!-- Never use color alone to convey information -->
<p style="color: red;">Error</p> <!-- Bad: color only -->
<p>
<span aria-hidden="true">⚠ </span>
<strong style="color: red;">Error:</strong> Name is required.
</p> <!-- Good: icon + bold + color -->
<!-- Screen reader utility class -->
<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>
<span class="sr-only">Loading, please wait...</span>Accessible Forms & Error Handling
<!-- Every input MUST have a label -->
<!-- Method 1: for + id -->
<label for="email">Email address</label>
<input type="email" id="email" name="email">
<!-- Method 2: wrapping label -->
<label>
Email address
<input type="email" name="email">
</label>
<!-- Method 3: aria-label (last resort, not visible) -->
<input type="search" aria-label="Search products" name="q">
<!-- Required field indication -->
<label for="name">
Full Name
<span aria-hidden="true"> *</span> <!-- Asterisk for sighted users -->
</label>
<input type="text" id="name" required aria-required="true">
<p id="required-note">Fields marked <span aria-hidden="true">*</span> are required</p>
<!-- Accessible error messages -->
<input
type="email"
id="email"
aria-invalid="true"
aria-describedby="email-error"
aria-required="true"
>
<!-- role="alert" announces immediately when inserted into DOM -->
<p id="email-error" role="alert" style="color: red;">
<span aria-hidden="true">✕ </span>
Please enter a valid email address.
</p>
<!-- Success feedback -->
<div role="status" aria-live="polite">
Your changes have been saved.
</div>Keep your own version of these notes — editable, searchable, and organised by your stack.
Start free