Skip to main content

WCAG Dev Examples

Interactive code examples for ARIA attributes, semantic HTML, focus management, and keyboard patterns.

27 examples

aria-expanded

Communicate whether a collapsible element is currently open or closed so assistive technology can announce the state.

Disclosure Button

4.1.2Level A
A button that toggles the visibility of a content panel. The expanded/collapsed state must be conveyed programmatically.

Button with aria-expanded and aria-controls

HTML
<button
  type="button"
  aria-expanded="false"
  aria-controls="panel1"
>
  Show details
</button>
<div id="panel1" hidden>
  <p>Additional details go here.</p>
</div>

Accordion Panel

4.1.2Level A
An accordion with multiple collapsible sections. Each header button must reflect its expanded state.

Buttons with aria-expanded inside heading elements

HTML
<h3>
  <button
    type="button"
    aria-expanded="true"
    aria-controls="sect1"
  >
    Section One
  </button>
</h3>
<div id="sect1" role="region" aria-labelledby="sect1-btn">
  <p>Content for section one.</p>
</div>

Dropdown Menu

4.1.2Level A
A navigation menu that expands from a button. The trigger must announce both its popup nature and current state.

Button with aria-expanded and aria-haspopup

HTML
<button
  type="button"
  aria-expanded="false"
  aria-haspopup="true"
  aria-controls="nav-menu"
>
  Menu
</button>
<ul id="nav-menu" role="menu" hidden>
  <li role="menuitem"><a href="/home">Home</a></li>
  <li role="menuitem"><a href="/about">About</a></li>
</ul>

aria-label & aria-labelledby

Provide accessible names for elements that lack visible text labels or need disambiguation.

Icon Button

4.1.2Level A
A button whose only visible content is an SVG icon. Without an accessible name, screen readers announce nothing useful.

Button with aria-label providing the accessible name

HTML
<button type="button" aria-label="Close dialog">
  <svg aria-hidden="true" viewBox="0 0 24 24">
    <path d="M6 18L18 6M6 6l12 12" />
  </svg>
</button>

Multiple Navigation Landmarks

1.3.1Level A
When a page has more than one <nav>, each must be labelled so users can distinguish them in the landmarks list.

Distinct aria-labels on each nav element

HTML
<nav aria-label="Main">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/products">Products</a></li>
  </ul>
</nav>

<nav aria-label="Footer">
  <ul>
    <li><a href="/privacy">Privacy</a></li>
    <li><a href="/terms">Terms</a></li>
  </ul>
</nav>

Dialog Title

4.1.2Level A
A modal dialog should be labelled by its visible heading so screen readers announce the dialog purpose on open.

Dialog labelled by its heading via aria-labelledby

HTML
<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
>
  <h2 id="dialog-title">Confirm Deletion</h2>
  <p>Are you sure you want to delete this item?</p>
  <button type="button">Delete</button>
  <button type="button">Cancel</button>
</div>

aria-hidden

Remove purely decorative or redundant content from the accessibility tree without affecting visual rendering.

Decorative Icon

1.3.1Level A
Icons next to text labels are decorative. If not hidden, screen readers announce meaningless SVG content.

SVG hidden from assistive technology with aria-hidden

HTML
<button type="button">
  <svg aria-hidden="true" viewBox="0 0 24 24">
    <path d="M12 2l3 7h7l-5.5 4 2 7L12 16l-6.5 4 2-7L2 9h7z" />
  </svg>
  Add to favorites
</button>

Focusable + Hidden Antipattern

4.1.2Level A
WARNING: Never place aria-hidden on an ancestor of focusable elements. This creates ghost focus where the screen reader goes silent while keyboard focus lands on an invisible control.

aria-hidden only on non-focusable decorative elements

HTML
<div class="card">
  <img aria-hidden="true" src="decorative-bg.svg" alt="" />
  <button type="button">View details</button>
</div>

aria-live

Announce dynamic content updates to screen readers without moving focus away from the user's current position.

Toast Notification

4.1.3Level AA
A status message that appears dynamically. The live region container must exist in the DOM before the message is injected.

Status message inside a polite live region

HTML
<!-- Container present on page load (empty) -->
<div role="status" aria-live="polite" id="toast-region"></div>

<!-- When a toast appears, inject into the region -->
<div role="status" aria-live="polite" id="toast-region">
  <p>Settings saved successfully.</p>
</div>

Form Validation

4.1.3Level AA
Inline error messages must be announced immediately so users understand what went wrong without hunting for the error visually.

Error region with aria-live assertive

HTML
<label for="email">Email</label>
<input
  id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid="true"
/>
<div id="email-error" aria-live="assertive" role="alert">
  Please enter a valid email address.
</div>

Loading Spinner

4.1.3Level AA
When content is loading asynchronously, a live region should announce both the loading state and when loading completes.

Live region announcing loading and completion states

HTML
<div aria-live="polite" id="load-status">
  Loading results...
</div>
<div class="spinner" aria-hidden="true"></div>

<!-- After content loads, update the region -->
<div aria-live="polite" id="load-status">
  12 results loaded.
</div>

ARIA Roles & States

Apply correct ARIA roles and states when building custom interactive widgets to ensure they behave like native controls for assistive technology.

Custom Checkbox

4.1.2Level A
A div styled as a checkbox. Without the correct role, state, and keyboard handling, it is invisible to assistive technology.

Div with role, aria-checked, and keyboard support

HTML
<div
  role="checkbox"
  aria-checked="false"
  tabindex="0"
  aria-labelledby="cb-label"
  onkeydown="handleKeydown(event)"
  onclick="toggleCheck()"
>
  <span class="checkbox-icon"></span>
</div>
<span id="cb-label">Subscribe to newsletter</span>

Tab Interface

4.1.2Level A
A tabbed UI requires a full set of ARIA roles (tablist, tab, tabpanel), states (aria-selected), and relationships (aria-controls, aria-labelledby).

Complete ARIA tab pattern with proper roles

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

Alert Role

4.1.2Level A
Important messages that require immediate user attention should use role="alert" so screen readers interrupt and announce them.

Message container with role alert

HTML
<div role="alert">
  <strong>Error:</strong> Your session has expired.
  Please <a href="/login">log in again</a>.
</div>

Semantic HTML

Use native HTML elements for their built-in semantics, keyboard behaviour, and accessibility support instead of generic divs and spans.

Button vs Div

4.1.2Level A
Native <button> elements are focusable, activatable with Enter/Space, and announced as buttons. Divs provide none of this.

Native button element

HTML
<button type="button" onclick="save()">
  Save Changes
</button>

Landmarks

1.3.1Level A
Semantic landmark elements create a navigable page structure that screen reader users rely on to jump between major sections.

Semantic landmark elements for page structure

HTML
<header>
  <nav aria-label="Main">
    <a href="/">Home</a>
  </nav>
</header>
<main>
  <h1>Page Title</h1>
  <p>Page content here.</p>
</main>
<footer>
  <p>&copy; 2026 Company</p>
</footer>

Proper Heading Hierarchy

1.3.1Level A
Headings must follow a logical hierarchy (h1 > h2 > h3) without skipping levels. Screen reader users navigate by heading level.

Logical heading order without skipped levels

HTML
<h1>Annual Report</h1>
<h2>Financial Summary</h2>
<h3>Revenue</h3>
<p>Total revenue was $5M.</p>
<h3>Expenses</h3>
<p>Total expenses were $3M.</p>
<h2>Team Updates</h2>
<p>We hired 12 new engineers.</p>

Focus Management

Control where keyboard focus goes during page interactions so users never lose their place or encounter unreachable content.

Skip Link

2.4.1Level A
A visually hidden link at the top of the page that lets keyboard users bypass repetitive navigation and jump straight to the main content.

Visually hidden skip link that appears on focus

HTML
<a
  href="#main-content"
  class="skip-link"
>
  Skip to main content
</a>
<!-- style .skip-link offscreen, :focus brings it on-screen -->

<nav><!-- site navigation --></nav>

<main id="main-content" tabindex="-1">
  <h1>Page Title</h1>
</main>

Dialog Focus Trap

2.4.3Level A
When a modal dialog opens, focus must move into it and stay trapped inside until the dialog is closed, then return to the trigger.

Focus moves to dialog, trapped inside, restored on close

HTML
<!-- Trigger stores reference for focus restoration -->
<button type="button" id="open-dialog">
  Delete Item
</button>

<div role="dialog" aria-modal="true"
  aria-labelledby="dlg-title">
  <h2 id="dlg-title">Confirm Delete</h2>
  <p>This action cannot be undone.</p>
  <button type="button" id="confirm-btn">Delete</button>
  <button type="button" id="cancel-btn">Cancel</button>
</div>
<!-- JS: focus #confirm-btn on open, trap Tab,
     restore focus to #open-dialog on close -->

Focus After Delete

2.4.3Level A
After removing an item from a list, focus must be explicitly moved to a logical location so it does not get lost on the document body.

Focus moves to next item after deletion

HTML
<ul aria-label="Task list">
  <li>
    <span>Task A</span>
    <button type="button" aria-label="Delete Task A">
      Delete
    </button>
  </li>
  <li>
    <span>Task B</span>
    <button type="button" aria-label="Delete Task B">
      Delete
    </button>
  </li>
</ul>
<!-- JS: after deleting Task A, focus moves to
     the "Delete Task B" button or a heading -->

Forms & Errors

Ensure form inputs have visible labels, errors are programmatically linked, and required fields are communicated to all users.

Label Association

1.3.1Level A
Every input must have a programmatically associated label. Placeholder text alone is not a substitute for a label.

Label explicitly associated with input via for/id

HTML
<div class="field">
  <label for="email">Email address</label>
  <input
    type="email"
    id="email"
    name="email"
    autocomplete="email"
  />
</div>

Error Messaging

3.3.1Level A
Validation errors must be visible text linked to the input with aria-describedby and the input marked as invalid.

Error text linked to input via aria-describedby

HTML
<label for="email">Email address</label>
<input
  type="email"
  id="email"
  aria-describedby="email-error"
  aria-invalid="true"
/>
<p id="email-error" class="error">
  Please enter a valid email address.
</p>

Required Fields

3.3.2Level A
Required fields must be indicated both visually and programmatically. A red asterisk without context is not sufficient.

Required attribute, aria-required, and visible text

HTML
<p class="form-note">
  Fields marked with <abbr title="required">*</abbr>
  are required.
</p>
<label for="name">
  Full name <abbr title="required">*</abbr>
</label>
<input
  type="text"
  id="name"
  required
  aria-required="true"
/>

Keyboard Interaction

All interactive content must be operable with a keyboard alone, using conventional key mappings that users expect.

Escape to Close

2.1.1Level A
Modals and popups must close when the user presses Escape, and focus must return to the element that triggered the overlay.

Escape handler closes the modal and restores focus

HTML
<div role="dialog" aria-modal="true"
  aria-labelledby="modal-title">
  <h2 id="modal-title">Settings</h2>
  <button type="button">Save</button>
  <button type="button" id="close-btn">Close</button>
</div>
<!-- JS: document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    closeModal();
    triggerButton.focus();
  }
}); -->

Roving Tabindex

2.1.1Level A
In composite widgets like toolbars or radio groups, only one item is in the tab order at a time. Arrow keys move focus between items.

Active item tabindex 0, others tabindex -1, arrow keys navigate

HTML
<div role="toolbar" aria-label="Text formatting">
  <button type="button" tabindex="0"
    aria-pressed="false">Bold</button>
  <button type="button" tabindex="-1"
    aria-pressed="false">Italic</button>
  <button type="button" tabindex="-1"
    aria-pressed="false">Underline</button>
</div>
<!-- JS: Arrow Left/Right moves tabindex="0"
     between buttons, Home/End jump to
     first/last -->

Dynamic Content

Content that updates or changes dynamically must convey its current value and purpose to assistive technology.

Progress Bar

4.1.2Level A
A file upload progress indicator. The progressbar role with current, min, and max values lets screen readers announce percentage completion.

Div with progressbar role and value attributes

HTML
<div
  role="progressbar"
  aria-valuenow="45"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-label="Upload progress"
>
  <div class="bar" style="width: 45%"></div>
</div>

Sortable Table Headers

4.1.2Level A
Clickable table headers that sort data must be buttons and must communicate the current sort direction.

Button inside th with aria-sort indicating direction

HTML
<table>
  <thead>
    <tr>
      <th aria-sort="ascending">
        <button type="button">Name</button>
      </th>
      <th aria-sort="none">
        <button type="button">Date</button>
      </th>
    </tr>
  </thead>
  <tbody>
    <tr><td>Alice</td><td>2026-01-15</td></tr>
    <tr><td>Bob</td><td>2026-03-22</td></tr>
  </tbody>
</table>