Skip to main content

The Complete A11y Reference

Every accessibility pattern, code example, and WCAG criterion in one place. Ask any question and get an instant answer with working code.

Try:

Foundations

Semantic HTML

The most powerful accessibility tool is already built into HTML. Using the right element for the right job gives screen readers, search engines, and browsers the context they need — for free, with no ARIA required.

Document structure

HTML
<!-- Good: landmark regions give structure -->
<header>
  <nav aria-label="Main navigation">
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/about">About</a></li>
    </ul>
  </nav>
</header>

<main id="main-content">
  <article>
    <h1>Page title</h1>
    <section aria-labelledby="section-heading">
      <h2 id="section-heading">Section title</h2>
      <p>Content...</p>
    </section>
  </article>
</main>

<footer>
  <p>&copy; 2025 Company</p>
</footer>

Heading hierarchy

Never skip heading levels. Going from h1 → h3 confuses screen reader users who navigate by headings. Use CSS to control size, not heading levels.
HTML
<!-- Bad: skipping heading levels -->
<h1>Page title</h1>
<h3>Section</h3>   <!-- jumped from h1 to h3 -->

<!-- Good: logical hierarchy -->
<h1>Page title</h1>
  <h2>Section</h2>
    <h3>Subsection</h3>
  <h2>Another section</h2>

Interactive elements

HTML
<!-- Bad: div used as button -->
<div onclick="submit()">Submit</div>

<!-- Good: use the native element -->
<button type="submit">Submit</button>

<!-- If you MUST use a div as a button -->
<div role="button" tabindex="0"
     onkeydown="if(e.key==='Enter'||e.key===' ')submit()"
     onclick="submit()">
  Submit
</div>

Foundations

ARIA Landmarks

Landmarks carve the page into navigable sections. Screen reader users rely on them to jump around quickly — similar to how sighted users scan visually.

HTML
<!-- HTML5 elements map to landmark roles automatically -->
<header>     <!-- role="banner" (only when top-level) -->
<nav>        <!-- role="navigation" -->
<main>       <!-- role="main" -->
<aside>      <!-- role="complementary" -->
<footer>     <!-- role="contentinfo" (only when top-level) -->
<section>    <!-- role="region" only when given a label -->

<!-- When you have multiple navs, label them -->
<nav aria-label="Main navigation">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<nav aria-label="Pagination">...</nav>

<!-- Section needs a label to become a landmark -->
<section aria-labelledby="news-heading">
  <h2 id="news-heading">Latest news</h2>
</section>
Rule of thumb: one <main> per page. Multiple <nav> elements are fine — just label each one uniquely.

Foundations

ARIA Roles

Use ARIA roles only when native HTML cannot express the pattern. ARIA helps screen readers understand custom components.

First rule of ARIA: don't use ARIA when native HTML works. Adding role="button" to a div still requires you to manually handle keyboard events, focus, and state.
HTML
<!-- Widget roles -->
<div role="button">        <!-- interactive button -->
<div role="checkbox">      <!-- checkable item -->
<div role="combobox">      <!-- editable dropdown -->
<div role="listbox">       <!-- list of selectable options -->
<div role="menu">          <!-- context/action menu -->
<div role="menuitem">      <!-- item in a menu -->
<div role="option">        <!-- option in a listbox/combobox -->
<div role="progressbar">   <!-- progress indicator -->
<div role="switch">        <!-- toggle on/off -->
<div role="tab">           <!-- a tab in a tablist -->
<div role="tablist">       <!-- container of tabs -->
<div role="tabpanel">      <!-- content for a tab -->
<div role="tooltip">       <!-- tooltip -->

<!-- Document roles -->
<div role="alert">         <!-- urgent message (assertive) -->
<div role="dialog">        <!-- modal or non-modal dialog -->
<div role="status">        <!-- non-urgent status message -->

Foundations

ARIA States & Properties

States describe the current condition of a component. Properties describe its nature. Both must be updated dynamically as the UI changes.

HTML
<!-- Labelling -->
<button aria-label="Close dialog">✕</button>
<input aria-labelledby="label-id">
<input aria-describedby="help-text-id">

<!-- State -->
<button aria-expanded="false">Show more</button>
<div aria-hidden="true">Decorative, hidden from AT</div>
<li role="option" aria-selected="true">Option A</li>
<input type="checkbox" aria-checked="mixed"> <!-- indeterminate -->
<button aria-disabled="true">Can't do this</button>
<input aria-required="true">
<input aria-invalid="true" aria-describedby="err">
<span id="err" role="alert">Required field</span>
JAVASCRIPT
// Dynamic update in JS
const btn = document.querySelector('#toggle');
btn.addEventListener('click', () => {
  const expanded = btn.getAttribute('aria-expanded') === 'true';
  btn.setAttribute('aria-expanded', !expanded);
  document.getElementById('panel').hidden = expanded;
});

Interaction

Keyboard Navigation

All functionality must be operable by keyboard alone. WCAG 2.1.1 (Level A) requires no mouse dependency.

Standard keyboard conventions

KeyExpected action
TabMove focus to next focusable element
Shift+TabMove focus to previous focusable element
EnterActivate a button, link, or submit a form
SpaceActivate a button; toggle a checkbox
Arrow keysNavigate within a widget (tabs, menus, sliders)
EscapeClose a dialog, menu, or tooltip
Home / EndJump to first/last item in a list

tabindex rules

HTML
<!-- tabindex="0" — add to tab order (natural DOM position) -->
<div role="button" tabindex="0">Custom button</div>

<!-- tabindex="-1" — focusable by script, not by Tab -->
<div id="panel" tabindex="-1">Dialog panel</div>

<!-- tabindex="1+" — AVOID. Forces unnatural tab order -->
<button tabindex="3">Don't do this</button>

Keyboard event handling

JAVASCRIPT
// Handle both Enter and Space for custom buttons
element.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    handleAction();
  }
});

// Arrow key navigation in a menu
const items = menu.querySelectorAll('[role="menuitem"]');
let currentIndex = 0;

menu.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowDown') {
    e.preventDefault();
    currentIndex = (currentIndex + 1) % items.length;
    items[currentIndex].focus();
  }
  if (e.key === 'ArrowUp') {
    e.preventDefault();
    currentIndex = (currentIndex - 1 + items.length) % items.length;
    items[currentIndex].focus();
  }
  if (e.key === 'Escape') closeMenu();
});

Interaction

Focus Management

Managing focus means moving it deliberately when the UI changes — opening a modal, loading new content, or completing a task.

Never do this: :focus { outline: none } — this removes focus visibility for keyboard users. WCAG 2.4.11 (AA) requires visible focus indicators.
CSS
/* Good: visible and branded focus styles */
:focus-visible {
  outline: 3px solid #1746a2;
  outline-offset: 3px;
  border-radius: 3px;
}

/* For dark backgrounds */
.dark-bg:focus-visible {
  outline: 3px solid #fff;
  box-shadow: 0 0 0 5px #1746a2;
}

Focus trapping (modals)

JAVASCRIPT
function trapFocus(element) {
  const focusable = [...element.querySelectorAll(
    'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'
  )];
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  element.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey) {
      if (document.activeElement === first) {
        last.focus(); e.preventDefault();
      }
    } else {
      if (document.activeElement === last) {
        first.focus(); e.preventDefault();
      }
    }
  });
}


Content

Forms & Labels

Every input must have a programmatic label, errors must be clearly associated with their fields, and required fields must be communicated to all users.

HTML
<!-- Method 1: explicit label (preferred) -->
<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 (when no visible label) -->
<input type="search" aria-label="Search products">

<!-- Placeholder is NOT a label -->
<input placeholder="Email"> <!-- bad alone — pair with a label -->

<!-- Required fields + error state -->
<label for="name">
  Full name <span aria-hidden="true">*</span>
</label>
<input type="text" id="name" required aria-required="true"
       aria-invalid="true" aria-describedby="name-error">
<p id="name-error" role="alert">This field is required</p>

<!-- Group related inputs -->
<fieldset>
  <legend>Notification preferences</legend>
  <label><input type="checkbox" name="email-notify"> Email</label>
  <label><input type="checkbox" name="sms-notify"> SMS</label>
</fieldset>

Content

Images & Alt Text

Every meaningful image needs alternative text. The quality of alt text matters as much as its presence — it should convey purpose, not just describe literally.

HTML
<!-- Meaningful image: describe purpose, not appearance -->
<img src="logo.png" alt="a11ytest.ai">

<!-- Decorative image: empty alt suppresses from screen readers -->
<img src="decoration.svg" alt="">

<!-- Functional image (inside a link): describe destination -->
<a href="/dashboard">
  <img src="icon-dashboard.svg" alt="Dashboard">
</a>

<!-- Complex image: use figcaption for long description -->
<figure>
  <img src="chart.png"
       alt="Sales chart for Q3 2025"
       aria-describedby="chart-desc">
  <figcaption id="chart-desc">
    Line chart showing revenue growing from £120k in July
    to £180k in September.
  </figcaption>
</figure>

<!-- Inline SVG: use title + desc -->
<svg role="img" aria-labelledby="svg-title svg-desc">
  <title id="svg-title">Upload progress</title>
  <desc id="svg-desc">65% of file uploaded</desc>
</svg>

<!-- Decorative SVG: hide completely -->
<svg aria-hidden="true" focusable="false">...</svg>
Don't start alt text with "Image of" or "Photo of" — screen readers already announce "image". For logos, just use the brand name.

Content

Colour & Contrast

Roughly 300 million people have colour vision deficiency. WCAG contrast requirements ensure text is readable for users with low vision or on poor screens.

Content typeLevel AALevel AAA
Normal text (under 18pt / 14pt bold)4.5:17:1
Large text (18pt+ or 14pt+ bold)3:14.5:1
UI components & graphics3:1
Focus indicator (WCAG 2.4.11)3:1
Don't rely on colour alone. WCAG 1.4.1: if colour is the only way to convey information (e.g. red = error), users with colour blindness will miss it. Add icons, patterns, or text labels alongside colour.

Content

Live Regions

Live regions announce dynamic content changes to screen reader users without requiring a page reload or focus change.

HTML
<!-- aria-live="polite": announces after user is idle -->
<div aria-live="polite" aria-atomic="true" id="status"></div>

<!-- role="status" = polite live region (shorthand) -->
<div role="status">3 results found</div>

<!-- role="alert" = assertive, interrupts immediately -->
<div role="alert">Error: session expired</div>
JAVASCRIPT
function announce(message, priority = 'polite') {
  const el = document.createElement('div');
  el.setAttribute('aria-live', priority);
  el.setAttribute('aria-atomic', 'true');
  el.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)';
  document.body.appendChild(el);
  setTimeout(() => { el.textContent = message; }, 100);
  setTimeout(() => { el.remove(); }, 5000);
}

announce('Form submitted successfully');
announce('Error: email already in use', 'assertive');

Components

Modals & Dialogs

Accessible modals require three things: move focus in on open, trap focus inside, and return focus to the trigger on close.

HTML
<button id="open-modal">Open dialog</button>

<div role="dialog"
     id="my-modal"
     aria-modal="true"
     aria-labelledby="modal-title"
     aria-describedby="modal-desc"
     hidden>
  <h2 id="modal-title">Confirm deletion</h2>
  <p id="modal-desc">This action cannot be undone.</p>
  <button type="button" id="confirm-btn">Delete</button>
  <button type="button" id="cancel-btn">Cancel</button>
  <button type="button" aria-label="Close dialog">✕</button>
</div>
JAVASCRIPT
const modal = document.getElementById('my-modal');
let previousFocus;

function openModal() {
  previousFocus = document.activeElement;  // remember trigger
  modal.hidden = false;
  modal.querySelector('button').focus();   // move focus in
  document.addEventListener('keydown', handleEscape);
}

function closeModal() {
  modal.hidden = true;
  document.removeEventListener('keydown', handleEscape);
  previousFocus?.focus();                  // return focus to trigger
}

function handleEscape(e) {
  if (e.key === 'Escape') closeModal();
}
Use the native <dialog> element where possible — it handles focus trapping, aria-modal, and Escape for you automatically.

Components

Tabs

The ARIA tab pattern uses a tablist container, individual tab elements, and associated tabpanel regions. Arrow keys navigate between tabs.

HTML
<div role="tablist" aria-label="Account settings">
  <button role="tab"
          id="tab-profile"
          aria-controls="panel-profile"
          aria-selected="true"
          tabindex="0">
    Profile
  </button>
  <button role="tab"
          id="tab-security"
          aria-controls="panel-security"
          aria-selected="false"
          tabindex="-1">
    Security
  </button>
</div>

<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
  Profile content...
</div>
<div role="tabpanel" id="panel-security" aria-labelledby="tab-security" hidden>
  Security content...
</div>
JAVASCRIPT
const tabs = [...tablist.querySelectorAll('[role="tab"]')];

function activateTab(tab) {
  tabs.forEach(t => {
    t.setAttribute('aria-selected', 'false');
    t.setAttribute('tabindex', '-1');
    document.getElementById(t.getAttribute('aria-controls')).hidden = true;
  });
  tab.setAttribute('aria-selected', 'true');
  tab.setAttribute('tabindex', '0');
  document.getElementById(tab.getAttribute('aria-controls')).hidden = false;
  tab.focus();
}

tablist.addEventListener('keydown', (e) => {
  const i = tabs.indexOf(document.activeElement);
  if (e.key === 'ArrowRight') activateTab(tabs[(i + 1) % tabs.length]);
  if (e.key === 'ArrowLeft')  activateTab(tabs[(i - 1 + tabs.length) % tabs.length]);
});

Components

Accordion

Accordion headers are buttons with aria-expanded controlling the visibility of their associated panel.

HTML
<h3>
  <button type="button"
    aria-expanded="false"
    aria-controls="panel-1"
    id="btn-1">
    What is WCAG?
  </button>
</h3>
<div id="panel-1" role="region" aria-labelledby="btn-1" hidden>
  <p>WCAG stands for Web Content Accessibility Guidelines...</p>
</div>
JAVASCRIPT
document.querySelectorAll('.accordion button').forEach(btn => {
  btn.addEventListener('click', () => {
    const expanded = btn.getAttribute('aria-expanded') === 'true';
    const panel = document.getElementById(btn.getAttribute('aria-controls'));
    btn.setAttribute('aria-expanded', !expanded);
    panel.hidden = expanded;
  });
});

Reference

WCAG Quick Reference

WCAG 2.2 (October 2023) has four principles: Perceivable, Operable, Understandable, and Robust. Most regulations require Level AA.

CriterionLevelSummary
1.1.1 Non-text ContentAAll non-text content has a text alternative
1.3.1 Info and RelationshipsAStructure conveyed visually is in the code
1.4.1 Use of ColourAColour not the sole means of conveying info
1.4.3 Contrast (Minimum)AA4.5:1 for normal text, 3:1 for large text
1.4.4 Resize TextAAText resizable to 200% without loss of content
1.4.10 ReflowAANo horizontal scroll at 320px width
1.4.11 Non-text ContrastAA3:1 for UI components and graphics
2.1.1 KeyboardAAll functionality available by keyboard
2.4.1 Bypass BlocksASkip links to bypass repeated navigation
2.4.3 Focus OrderAFocus order preserves meaning and operability
2.4.7 Focus VisibleAAKeyboard focus indicator visible
2.4.11 Focus AppearanceNew 2.2AAFocus indicator meets minimum size and contrast
2.5.7 Dragging MovementsNew 2.2AAAll drag actions have a pointer alternative
2.5.8 Target SizeNew 2.2AAMinimum 24×24px touch target size
3.1.1 Language of PageAlang attribute on <html>
3.3.1 Error IdentificationAErrors identified in text
3.3.2 Labels or InstructionsALabels provided for user inputs
3.3.7 Redundant EntryNew 2.2ADon't ask for the same info twice in a process
4.1.2 Name, Role, ValueAUI components expose name, role, state to AT
4.1.3 Status MessagesAAStatus messages programmatically determined

Reference

Testing Checklist

A good accessibility test combines automated scanning (catches ~30–40% of issues) with manual keyboard and screen reader testing.

#TestWhat to check
1Keyboard onlyTab through every interactive element. Is focus always visible? Can you reach and operate everything?
2Screen reader (NVDA/VoiceOver)Navigate by headings, landmarks, links. Do all images, buttons, and forms make sense?
3200% zoomZoom to 200% in browser. Does content reflow? Does anything overlap or disappear?
41.5× text spacingApply text spacing bookmarklet. Check nothing breaks.
5High contrast modeEnable Windows High Contrast. Custom colours should not break content.
6Mobile + switchTest with iOS Switch Control or Android Switch Access.

Scan your site for accessibility issues

a11ytest.ai catches WCAG failures, generates fix suggestions, and integrates with your CI/CD pipeline.

Get started →