HTML
03 / 12

Accessibility (a11y)

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