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 AButton with aria-expanded and aria-controls
<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 AButtons with aria-expanded inside heading elements
<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 AButton with aria-expanded and aria-haspopup
<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 AButton with aria-label providing the accessible name
<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 ADistinct aria-labels on each nav element
<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 ADialog labelled by its heading via aria-labelledby
<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 ASVG hidden from assistive technology with aria-hidden
<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 Aaria-hidden only on non-focusable decorative elements
<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 AAStatus message inside a polite live region
<!-- 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 AAError region with aria-live assertive
<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 AALive region announcing loading and completion states
<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 ADiv with role, aria-checked, and keyboard support
<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 AComplete ARIA tab pattern with proper roles
<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 AMessage container with role alert
<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 ANative button element
<button type="button" onclick="save()">
Save Changes
</button>Landmarks
1.3.1Level ASemantic landmark elements for page structure
<header>
<nav aria-label="Main">
<a href="/">Home</a>
</nav>
</header>
<main>
<h1>Page Title</h1>
<p>Page content here.</p>
</main>
<footer>
<p>© 2026 Company</p>
</footer>Proper Heading Hierarchy
1.3.1Level ALogical heading order without skipped levels
<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 AVisually hidden skip link that appears on focus
<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 AFocus moves to dialog, trapped inside, restored on close
<!-- 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 AFocus moves to next item after deletion
<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 ALabel explicitly associated with input via for/id
<div class="field">
<label for="email">Email address</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
/>
</div>Error Messaging
3.3.1Level AError text linked to input via aria-describedby
<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 ARequired attribute, aria-required, and visible text
<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 AEscape handler closes the modal and restores focus
<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 AActive item tabindex 0, others tabindex -1, arrow keys navigate
<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 ADiv with progressbar role and value attributes
<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 AButton inside th with aria-sort indicating direction
<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>