// ARIA + Semantic HTML cheat sheet — quick reference for agents & developers
| UI pattern | Correct role(s) | Notes |
|---|---|---|
| Toggle options (multi) | group + input[checkbox] | Each option is independently on/off |
| Pick one from a list | listbox + option | Like a styled <select>; use aria-selected |
| Pick many from a list | listbox + option + aria-multiselectable | Drop visual checkboxes; use aria-selected only |
| Collapsible section | button[aria-expanded] + region or group | Never role="button" on a div if you can use <button> |
| Modal dialog | dialog + aria-modal="true" | Trap focus; aria-labelledby title |
| Tooltip | tooltip (on popup) + aria-describedby (on trigger) | Not for interactive content |
| Tab switcher | tablist + tab + tabpanel | Arrow keys between tabs; Tab into panel |
| Accordion | button[aria-expanded] per item | No special role on wrapper needed |
| Alert / toast | alert or status | alert = assertive; status = polite |
| Navigation menu | nav + ul/li or menubar + menuitem | menubar only for app-style menus, not site nav |
| Combobox / autocomplete | combobox + listbox + option | aria-expanded, aria-controls, aria-activedescendant |
| Breadcrumbs | nav[aria-label="Breadcrumb"] + ol + aria-current="page" | Last item gets aria-current |
| Progress bar | progressbar + aria-valuenow/min/max | Add aria-valuetext for human label |
| Slider | slider + aria-valuenow/min/max/valuetext | Arrow keys move value; Home/End jump to limits |
| Switch (toggle) | switch + aria-checked | NOT checkbox — implies on/off system setting |
| Tree / tree item | tree + treeitem + group | aria-expanded on parent nodes; arrow keys navigate |
| Data table | table + columnheader/rowheader + gridcell | Use scope="col/row" on th elements |
| Sortable table | columnheader + aria-sort | Values: ascending, descending, none, other |
| Search input | search (landmark) + input[type=search] | Wrap in <search> or role="search" |
| HTML element | Implicit role | aria-label needed? | Rule |
|---|---|---|---|
| <header> | banner | No (1 per page) | Top-level only; inside <article> it has no role |
| <nav> | navigation | Yes if multiple | Label distinguishes "Main nav" vs "Breadcrumb" |
| <main> | main | No | Only one per page |
| <aside> | complementary | Yes if multiple | — |
| <footer> | contentinfo | No (1 per page) | Top-level only |
| <search> / role="search" | search | Optional | New HTML element; wraps search form |
| <section> | region (if labelled) | Required for role | Without label it has no landmark role |
| <form> | form (if labelled) | Required for role | Without label it has no landmark role |
| <article> | article | Optional | Self-contained; can nest |
<div role="dialog" aria-modal="true" aria-labelledby="dlg-title" tabindex="-1"> <h2 id="dlg-title">Title</h2> /* Move focus here on open */ /* Trap Tab/Shift+Tab inside */ /* Restore focus on close */ </div>
/* Only one item in tab sequence at a time */ <div role="toolbar"> <button tabindex="0">Active</button> <button tabindex="-1">Inactive</button> <button tabindex="-1">Inactive</button> </div> /* Arrow keys move tabindex="0" */ /* Tab exits the whole widget */
| Widget | Key | Expected behaviour |
|---|---|---|
| Button | Enter Space | Activate |
| Checkbox | Space | Toggle checked |
| Radio group | ↑ ↓ | Move selection; Tab exits group |
| Select / Listbox | ↑ ↓ Home End | Navigate; Space/Enter select |
| Tab list | ↑↓ or ←→ | Move between tabs; Tab enters panel |
| Dialog | Esc | Close and return focus to trigger |
| Combobox | ↓ opens · Esc closes · Enter selects | Type filters list |
| Tree | → expand · ← collapse · ↑↓ navigate | Home/End to first/last node |
| Slider | ←→ or ↑↓ | ±1 step · Home/End min/max · Page±10% |
| Menu / Menubar | ←→ top items · ↑↓ sub items | Esc closes; Tab exits menubar |
<div> <button aria-expanded="false" aria-controls="panel-1" id="btn-1"> Section title </button> <div id="panel-1" role="region" aria-labelledby="btn-1" hidden> Content... </div> </div>
<div role="combobox" aria-expanded="true" aria-haspopup="listbox" aria-owns="opts"> <input type="text" aria-autocomplete="list" aria-activedescendant="opt2"/> </div> <ul role="listbox" id="opts"> <li role="option" id="opt1">Apple</li> <li role="option" id="opt2" aria-selected="true">Banana</li> </ul>
<div role="tablist" aria-label="Settings"> <button role="tab" aria-selected="true" aria-controls="panel-a" tabindex="0">General</button> <button role="tab" aria-selected="false" aria-controls="panel-b" tabindex="-1">Security</button> </div> <div role="tabpanel" id="panel-a" aria-labelledby="tab-a" tabindex="0">...</div>
<div> <label for="email">Email</label> <input id="email" type="email" aria-describedby="email-hint email-err" aria-invalid="true" aria-required="true"/> <span id="email-hint">e.g. you@example.com</span> <span id="email-err" role="alert">Invalid email</span> </div>
<button>, <input>, <select> before reaching for role=. Native elements give you keyboard, focus, and AT support for free.<div> is invisible to AT. A <div role="button"> without tabindex/keyboard is broken. Broken ARIA is worse than no ARIA.aria-label. Groups need aria-labelledby.outline: none without a custom focus style. Every interactive element must have a visible focus indicator.aria-hidden="true" removes from AT tree only. Element is still visible and focusable. Never put it on a focusable element.aria-live regions in the DOM on page load. Inserting them dynamically before updating means AT misses the first announcement.display:none or the hidden attribute to truly hide collapsed content. visibility:hidden / opacity:0 may not be enough./* Visually hidden but announced by screen readers */ .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; } /* Becomes visible on focus — for skip links */ .sr-only-focusable:focus { position: static; width: auto; height: auto; overflow: visible; clip: auto; white-space: normal; }
<html lang="en"> <head> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>Page — Site</title> </head> <body> <a href="#main" class="sr-only-focusable"> Skip to content </a> <header>...</header> <nav aria-label="Main">...</nav> <main id="main">...</main> <footer>...</footer> </body> </html>
<article> <header> <h1>Article title</h1> <p>By <address> <a rel="author" href="/authors/jane"> Jane Smith </a> </address> </p> <time datetime="2025-03-01"> March 2025 </time> </header> <p>Content...</p> <footer>Tags, related...</footer> </article>
| Element | Use for | Notes / common mistakes |
|---|---|---|
<main> | Primary page content | One per page; skip-link target |
<header> | Page or section header | Implicit banner role only at top level |
<footer> | Page or section footer | Implicit contentinfo only at top level |
<nav> | Navigation landmarks | Label with aria-label if more than one |
<aside> | Tangentially related content | Sidebars, pull quotes, callouts |
<article> | Self-contained content | Posts, cards, comments — can nest |
<section> | Thematic grouping with a heading | Needs aria-labelledby for landmark role; don't use as div |
<h1>–<h6> | Heading hierarchy | One h1 per page; don't skip levels |
<p> | Paragraphs of text | Don't use for spacing — use CSS |
<ul> / <ol> | Unordered / ordered list | Always use li as direct children |
<dl> | Description list (term–value pairs) | Good for metadata, glossaries, key-value UI |
<figure> + <figcaption> | Image with caption | figcaption labels the figure for AT |
<picture> | Art-directed responsive images | Always include an <img> fallback with alt |
<time> | Dates and times | datetime attr must be machine-readable ISO 8601 |
<address> | Contact info for nearest <article> or <body> | Not for arbitrary postal addresses in content |
<blockquote> | Extended quotation | cite attr for source URL; use <cite> inside for attribution |
<cite> | Title of a creative work | Not for person names |
<abbr> | Abbreviation | title attr provides expansion |
<mark> | Highlighted / search match | Conveys relevance, not emphasis |
<strong> | Strong importance | Not just bold — conveys semantic weight |
<em> | Stress emphasis | Changes sentence meaning; not just italic |
<small> | Side comments, fine print | Not for decorative small text |
<details> + <summary> | Native disclosure widget | Free keyboard + AT support; no JS needed |
<dialog> | Modal or non-modal dialog | showModal() traps focus natively in modern browsers |
<search> | Search landmark wrapper | HTML 2023 — replaces role="search" on <form> |
<output> | Result of a calculation or action | Implicit aria-live="polite" |
<meter> | Scalar value in a known range | Disk usage, rating — NOT for progress |
<progress> | Completion progress | Indeterminate when no value attr |
<datalist> | Autocomplete suggestions for input | Link via input list="id" |
<fieldset> + <legend> | Group related form fields | legend is the group label for AT |
<h1>Site / Page title</h1> <h2>Major section</h2> <h3>Subsection</h3> <h3>Subsection</h3> <h2>Another section</h2> <h3>Subsection</h3> <h4>Deep subsection</h4> /* One h1 per page */ /* Never skip a level (h2→h4) */ /* Level = structure, not size */
<h1>Title</h1> <h1>Another h1</h1> <h4>Skipped h2 + h3</h4> /* Using h3 because it "looks */ /* right" in CSS — wrong. */ /* Style with CSS, not heading */ /* level. */ <p><b>Fake heading</b></p> /* AT can't detect this */
| Image type | Markup pattern | alt rule |
|---|---|---|
| Informative | <img src alt="description"> | Describe what the image conveys, not what it looks like |
| Decorative | <img src alt=""> | Empty alt — AT skips it entirely |
| Functional (button icon) | <img src alt="Search"> | Describe the action, not the icon ("Magnifying glass" is wrong) |
| Complex (chart, diagram) | <figure><img><figcaption> | Short alt + full description in figcaption or aria-details |
| SVG inline | <svg role="img" aria-label="..."> | Add title + desc inside SVG; aria-labelledby both ids |
| SVG decorative | <svg aria-hidden="true"> | Hide from AT completely |
| CSS background image | No img element | If meaningful, provide text alternative in DOM |
<form aria-labelledby="form-title"> <h2 id="form-title">Sign up</h2> <fieldset> <legend>Personal details</legend> <label for="name">Full name</label> <input id="name" type="text" autocomplete="name" required/> </fieldset> <button type="submit">Create account</button> </form>
<table> <caption>Q3 Sales by Region</caption> <thead> <tr> <th scope="col">Region</th> <th scope="col">Revenue</th> </tr> </thead> <tbody> <tr> <th scope="row">APAC</th> <td>$1.2M</td> </tr> </tbody> </table>
<caption> — it's the table's accessible namescope="col" on column headers, scope="row" on row headersid + headers attributes instead of scope<thead>, <tbody>, <tfoot> are structural — always include them<td> </td> or a meaningful aria-label| Use case | Element | Rule |
|---|---|---|
| Navigate to a URL | <a href="..."> | Always has an href; right-click menu works; opens in new tab |
| Trigger an action (no navigation) | <button type="button"> | Activated by Enter + Space; no href |
| Submit a form | <button type="submit"> | Default type inside <form> — always be explicit |
| Reset a form | <button type="reset"> | Rarely needed; usually a bad UX |
| Download a file | <a href download> | download attr triggers save dialog; aria-label the filename |
| External link | <a href target="_blank" rel="noopener noreferrer"> | Warn users: add "(opens in new tab)" in label |
| Anchor / jump link | <a href="#section-id"> | Target needs a matching id; move focus to target |
<a>. If it does something → <button>. Never a <div> or <span> for either.
<font> → use CSS font-* properties<center> → use CSS text-align: center<strike> → use <s> or CSS text-decoration<b> for importance → use <strong><i> for emphasis → use <em><acronym> → use <abbr><table> for layout → use CSS Grid/Flex<br> for spacing → use CSS margin/padding<hr> purely visual → use CSS border on a div/* No JS, no ARIA needed */ <details> <summary> Section title </summary> <p>Hidden content...</p> </details> /* Add open attr to start expanded */ <details open>...
<dialog id="dlg" aria-labelledby="dlg-h"> <h2 id="dlg-h">Title</h2> <p>Content</p> <button autofocus>Close</button> </dialog> // JS: showModal() traps focus // Esc closes natively // ::backdrop for overlay