Theme
On this page (11)

§Semantic HTML first

Components start from the most semantic native element available. A <button> is already a button - no role="button" needed. A <dialog> already traps focus and handles Escape. ARIA is only added when the native element doesn't express the full semantics of the widget.

<!-- Don't add ARIA roles to native elements -->
<div role="button" tabindex="0"
  onclick="...">
  Click me
</div>
<!-- Native button: focusable, keyboard
     activatable, announced correctly -->
<button class="btn">
  Click me
</button>

§ARIA roles

When no native element expresses the widget semantics, ARIA roles tell assistive technology what the element represents. These follow the WAI-ARIA Authoring Practices Guide (APG).

ARIA roles by component
RoleComponentPurpose
menuDropdown, Context MenuA list of actions or options
menuitemDropdown, Context MenuA single action in a menu
menuitemcheckboxDropdownA togglable menu item
menuitemradioDropdownA mutually exclusive menu option
tablistTabsContainer for tab triggers
tabTabsA single tab trigger
tabpanelTabsContent area associated with a tab
listboxComboboxA list of selectable options
optionComboboxA single option in a listbox
toolbarToolbarA container for grouped actions
treeitemTree ViewA node in a hierarchical tree
dialogDialog, SheetA modal or non-modal overlay (native via <dialog>)
alertdialogAlert DialogA dialog requiring acknowledgment
alertAlertAn important, time-sensitive message
statusToastA polite status notification

§State attributes

ARIA state attributes communicate dynamic state changes to assistive technology. These are managed by JavaScript at runtime.

ARIA state attributes
AttributeValuesUsage
aria-expandedtrue / falseDropdown triggers, popover triggers, accordion headers - indicates whether the controlled content is visible
aria-selectedtrue / falseTab triggers, combobox options - indicates the active selection
aria-checkedtrue / false / mixedMenu item checkboxes, menu item radios, toggle buttons
aria-pressedtrue / falseToggle buttons - indicates pressed/active state
aria-disabledtrueNon-native disabled elements (menu items, links) where the HTML disabled attribute isn't available
aria-invalidtrueForm inputs with validation errors
aria-busytrueLoading states - tells screen readers content is still loading

§Relationship attributes

These attributes create semantic connections between elements, allowing assistive technology to navigate related content.

relationship attributes
AttributePurposeExample
aria-controlsLinks a trigger to the element it controlsTab trigger → tab panel
aria-labelledbyLabels an element using another element's textTab panel labeled by its tab trigger
aria-describedbyProvides additional descriptive textInput described by its help text
aria-activedescendantPoints to the visually focused item in a composite widgetCombobox input → highlighted option
aria-haspopupIndicates a trigger will open a popupmenu, dialog, or listbox
aria-ownsCreates parent-child relationship in the accessibility treeCombobox input owns the listbox
<div role="tablist" aria-label="Settings">
  <button role="tab"
    id="tab-1"
    aria-selected="true"
    aria-controls="panel-1"
    tabindex="0">
    General
  </button>
  <button role="tab"
    id="tab-2"
    aria-selected="false"
    aria-controls="panel-2"
    tabindex="-1">
    Advanced
  </button>
</div>
<div role="tabpanel"
  id="panel-1"
  aria-labelledby="tab-1">
  General settings content
</div>
<div role="tabpanel"
  id="panel-2"
  aria-labelledby="tab-2"
  hidden>
  Advanced settings content
</div>

§Keyboard navigation

Every interactive component is fully keyboard accessible. The keyboard patterns follow WAI-ARIA APG conventions so users get consistent behavior across the web.

keyboard patterns by component type
KeyMenus & DropdownsTabsDialogs
↑↓Navigate between menu itemsNavigate tabs (vertical)—
←→—Navigate tabs (horizontal)—
HomeJump to first itemJump to first tab—
EndJump to last itemJump to last tab—
Enter / SpaceActivate itemActivate tabActivate focused button
EscapeClose menu, return focus to trigger—Close dialog, return focus to trigger
TabClose menu and move to next focusableMove into tab panelTrapped within dialog
Type-aheadJump to item starting with typed character——

§Roving tabindex

Composite widgets (tabs, toolbars, menus) use roving tabindex so the entire widget acts as a single tab stop. Only one item has tabindex="0" at a time - all others have tabindex="-1". Arrow keys move focus between items.

<!-- Only the active tab is in the tab order -->
<div role="tablist">
  <button role="tab" tabindex="0" aria-selected="true">Tab 1</button>
  <button role="tab" tabindex="-1" aria-selected="false">Tab 2</button>
  <button role="tab" tabindex="-1" aria-selected="false">Tab 3</button>
</div>
<!-- User presses → - focus moves to Tab 2:
     Tab 1: tabindex="-1", Tab 2: tabindex="0" -->
// Move focus to the item at `index` - df$ writes the attributes
function focusItem(items, index) {
  df$(items).attr('tabindex', '-1');
  df$(items[index]).attr('tabindex', '0');
  items[index].focus(); // focus stays a native call
}

Used by: Tabs, Toolbar, Toggle Group, Dropdown Menu, Tree View

§Focus management

When overlays open and close, focus must move predictably. Dialogs trap focus. When a modal closes, focus returns to the trigger that opened it.

Focus trapping<dialog> with .showModal() traps focus natively - Tab cycles within the dialog only. No JS focus-trap library needed.Focus restorationWhen a dialog or menu closes, focus returns to the trigger element that opened it. JS stores a reference to the trigger before opening.Focus-visible:focus-visible shows focus rings only on keyboard navigation, not mouse clicks. Applied to every interactive component.
// Store the trigger before opening
dialog._trigger = triggerButton;
dialog.showModal();
// Restore focus when the dialog closes
df$(dialog).on('close', () => dialog._trigger?.focus());

§Live regions

Live regions announce dynamic content changes to screen readers without moving focus. Toast notifications and alerts use these to ensure users are informed of updates.

aria-live="polite"Announced after the screen reader finishes its current output. Used for info and success toasts.aria-live="assertive"Interrupts the screen reader immediately. Used for error/destructive toasts and alerts.aria-atomic="true"The entire region is announced as a whole, not just the changed parts.
<!-- Success toast: polite announcement -->
<div class="toast" role="status"
  aria-live="polite" aria-atomic="true">
  Settings saved successfully.
</div>
<!-- Error toast: assertive (immediate) announcement -->
<div class="toast" data-variant="destructive"
  role="alert" aria-live="assertive" aria-atomic="true">
  Failed to save. Please try again.
</div>

§Screen reader considerations

A few patterns help screen readers present clean, meaningful content.

Decorative iconsIcons next to text labels get aria-hidden="true" so they're not announced redundantly.Icon-only buttonsButtons with no visible text need aria-label to provide an accessible name: <button aria-label="Close">.Visually hidden textText that should be announced but not visible uses the shipped .sr-only utility (see below).Form labelsEvery form input has a visible <label> element with a matching for/id pair, or aria-label for unlabeled inputs.
<!-- Icon-only button: needs aria-label -->
<button class="btn" data-size="icon" aria-label="Close">
  <svg aria-hidden="true">...</svg>
</button>
<!-- Button with text: the icon is decorative -->
<button class="btn">
  <svg aria-hidden="true">...</svg>
  Save Changes
</button>

§Screen-reader-only content

Some text belongs to the accessibility tree but not to the visual design: an extra word of context on a link, the unit a <meter> omits, the label an icon-only control needs. .sr-only ships in the optional theme/utils/accessibility.css module and clips content to a 1px box - CSS the browser still hands to assistive technology (unlike display:none or visibility:hidden, which hide it from screen readers too). .not-sr-only reverses it - the classic skip-link pattern. This very page links the module, so the demo below is the real utility, not a replica.

HTML
<a class="btn" data-variant="link" href="#">
  Read the changelog<span class="sr-only"> (opens the release history)</span>
</a>

The link reads identically on screen - a screen reader announces the extra phrase. Never put the only accessible name of a focusable control inside .sr-only; use aria-label for that.

§Load the accessibility module

HTML
<link rel="stylesheet" href="../theme/utils/default-semantic-tokens.css">
<!-- optional module: .sr-only / .not-sr-only -->
<link rel="stylesheet" href="../theme/utils/accessibility.css">

Each component's component-skill.md includes a dedicated ARIA section listing the exact attributes required for that component. See the WAI-ARIA APG for the full specifications these patterns implement.

Comments, ideas or improvements? Edit this page's source on GitHub