Accessibility pattern · Buttons and actions

Toolbar

The whole toolbar is a single stop in the Tab order: Left and Right move between its controls, and Tab leaves it. Inside, each control keeps its own keys: Up and Down pick an alignment, change the size, or open the color menu.

WCAG criteria
7
Keyboard rules
7
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Festive delivery

Diwali orders placed by 28 October arrive before the festival. Gift wrapping is free on every order this week.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Tab or ShiftTabMoves into the toolbar, onto the control used last, and out again. The toolbar is one stop.
Arrow Right or Arrow LeftMoves to the next or previous control, wrapping at the ends.
Home or EndMoves to the first or last control.
Enter or SpacePicks the focused alignment, or opens the color menu.
Arrow Down or Arrow UpIn the alignment group, picks the next or previous alignment. On the size, makes the text one pixel smaller or larger. On the color button, opens the menu.
Page Down or Page UpOn the size, makes the text four pixels smaller or larger.
EscapeCloses the color menu and returns focus to its button, or hides the tooltip.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the toolbarText formatting, toolbar. Text alignment, radio group. Align left, radio button, checked, 1 of 3
Arrow Right moves onAlign center, radio button, not checked, 2 of 3
Arrow Right reaches the sizeFont size in pixels, spin button, 16
Arrow Up changes it17
Arrow Right reaches the color buttonText color Ink, menu button, collapsed
A color is chosenText color Indigo, menu button, collapsed

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-toolbar" data-ap-toolbar>
  <div class="ap-toolbar__card">
    <div class="ap-toolbar__bar" role="toolbar" aria-label="Text formatting" aria-orientation="horizontal" aria-controls="toolbar-sample">
      <div class="ap-toolbar__radios" role="radiogroup" aria-label="Text alignment">
        <button type="button" class="ap-toolbar__btn" role="radio" aria-checked="true" aria-label="Align left" data-ap-align="start" data-ap-tip>
          <svg class="ap-toolbar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M4 10h10M4 14h16M4 18h10"/></svg>
        </button>
        <button type="button" class="ap-toolbar__btn" role="radio" aria-checked="false" aria-label="Align center" tabindex="-1" data-ap-align="center" data-ap-tip>
          <svg class="ap-toolbar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M7 10h10M4 14h16M7 18h10"/></svg>
        </button>
        <button type="button" class="ap-toolbar__btn" role="radio" aria-checked="false" aria-label="Align right" tabindex="-1" data-ap-align="end" data-ap-tip>
          <svg class="ap-toolbar__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 6h16M10 10h10M4 14h16M10 18h10"/></svg>
        </button>
      </div>

      <div class="ap-toolbar__sep" role="separator" aria-orientation="vertical"></div>

      <div class="ap-toolbar__size" data-ap-tip>
        <span class="ap-toolbar__step" data-ap-step="-1" aria-hidden="true">
          <svg viewBox="0 0 24 24" focusable="false"><path d="M6 12h12"/></svg>
        </span>
        <span class="ap-toolbar__spin" role="spinbutton" tabindex="-1" aria-label="Font size in pixels" aria-valuenow="16" aria-valuemin="12" aria-valuemax="28"><span data-ap-value translate="no">16</span><span class="ap-toolbar__unit" aria-hidden="true" translate="no">px</span></span>
        <span class="ap-toolbar__step" data-ap-step="1" aria-hidden="true">
          <svg viewBox="0 0 24 24" focusable="false"><path d="M6 12h12M12 6v12"/></svg>
        </span>
      </div>

      <div class="ap-toolbar__sep" role="separator" aria-orientation="vertical"></div>

      <div class="ap-toolbar__color">
        <button type="button" class="ap-toolbar__btn ap-toolbar__pick" id="toolbar-color-button" aria-haspopup="menu" aria-expanded="false" aria-controls="toolbar-colors" aria-labelledby="toolbar-color-label toolbar-color-name" tabindex="-1" data-ap-tip>
          <span class="ap-toolbar__sr" id="toolbar-color-label">Text color</span>
          <span class="ap-toolbar__glyph" aria-hidden="true">
            <svg viewBox="0 0 24 24" focusable="false"><path d="m6 17 6-13 6 13M8.3 12h7.4"/></svg>
            <span class="ap-toolbar__ink" data-color="ink"></span>
          </span>
          <span class="ap-toolbar__sr" id="toolbar-color-name">Ink</span>
          <svg class="ap-toolbar__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m7 10 5 5 5-5"/></svg>
        </button>
        <div class="ap-toolbar__menu" role="menu" id="toolbar-colors" aria-labelledby="toolbar-color-label" hidden>
          <button type="button" class="ap-toolbar__item" role="menuitemradio" aria-checked="true" tabindex="-1" data-color="ink">
            <span class="ap-toolbar__swatch" aria-hidden="true"></span><span class="ap-toolbar__label">Ink</span>
            <svg class="ap-toolbar__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
          </button>
          <button type="button" class="ap-toolbar__item" role="menuitemradio" aria-checked="false" tabindex="-1" data-color="indigo">
            <span class="ap-toolbar__swatch" aria-hidden="true"></span><span class="ap-toolbar__label">Indigo</span>
            <svg class="ap-toolbar__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
          </button>
          <button type="button" class="ap-toolbar__item" role="menuitemradio" aria-checked="false" tabindex="-1" data-color="green">
            <span class="ap-toolbar__swatch" aria-hidden="true"></span><span class="ap-toolbar__label">Green</span>
            <svg class="ap-toolbar__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
          </button>
          <button type="button" class="ap-toolbar__item" role="menuitemradio" aria-checked="false" tabindex="-1" data-color="amber">
            <span class="ap-toolbar__swatch" aria-hidden="true"></span><span class="ap-toolbar__label">Amber</span>
            <svg class="ap-toolbar__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
          </button>
          <button type="button" class="ap-toolbar__item" role="menuitemradio" aria-checked="false" tabindex="-1" data-color="red">
            <span class="ap-toolbar__swatch" aria-hidden="true"></span><span class="ap-toolbar__label">Red</span>
            <svg class="ap-toolbar__tick" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
          </button>
        </div>
      </div>
    </div>

    <div class="ap-toolbar__sample" id="toolbar-sample" data-align="start" data-color="ink">
      <p class="ap-toolbar__lead">Festive delivery</p>
      <p>Diwali orders placed by 28 October arrive before the festival. Gift wrapping is free on every order this week.</p>
    </div>
  </div>
  <span class="ap-toolbar__tip" aria-hidden="true" hidden></span>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The toolbar, the radio group and the separators have roles, so the grouping that is drawn is also exposed.

  • 1.4.1 Use of Color Level A

    The checked alignment is raised with an edge and a shadow, and the chosen color has a check mark, not just a tint.

  • 1.4.13 Content on Hover or Focus Level AA

    The labels that appear on hover or focus can be dismissed with Escape and stay while the pointer is over them.

  • 2.1.1 Keyboard Level A

    Every control works from the keyboard, with the keys each role is expected to answer to.

  • 2.4.3 Focus Order Level A

    The toolbar is one stop in the Tab order and remembers the last control used, so Tab never walks through every button.

  • 2.4.7 Focus Visible Level AA

    Each control shows a two-pixel focus ring, and the focused color in the menu is filled as well.

  • 4.1.2 Name, Role, Value Level A

    Alignment buttons are radios with aria-checked, the size is a spin button with its value, and the color button names the current color.

Usage

When to use it

Use it

  • Three or more controls that act on the same thing, like the formatting of a text or the view of a chart.
  • A dense set of tools that would cost too many Tab presses as separate stops.

Use something else

  • Two buttons side by side: leave them as plain buttons.
  • Controls that need text entry and the Left and Right keys, like a search field: put them outside the toolbar, or last in it.
  • A row of links to other pages: use a nav landmark.

Common failures

How it usually goes wrong

  • Every control a tab stop

    Ten tools in a row cost ten Tab presses to get past. A toolbar is one stop, with arrow keys inside, as people expect from desktop apps.

  • A toolbar with no name

    "Toolbar" alone does not say what it works on. aria-label names it, and aria-controls points at the text it changes.

  • Focus that forgets

    Returning to the toolbar should land on the control used last, not the first one. The roving tabindex here moves with focus, so it remembers.

  • Arrow keys taken by a field

    A text field inside a toolbar needs Left and Right to move its caret, which breaks moving between controls. The size here is a spin button, which uses Up and Down.

  • Icon-only controls with no visible label

    Sighted keyboard users cannot read an aria-label. A label appears beside each control on hover and on keyboard focus.

  • Separators read out as content

    Drawing groups with stray characters like "|" fills the reading order with noise. role=separator marks the break without words.

Notes

Building it

  • Set aria-orientation="horizontal" (the default for toolbars) so assistive technology knows which arrow keys move; a vertical toolbar would use Up and Down.
  • Home and End move within the toolbar, not to the size's minimum and maximum; Page Up and Page Down make bigger steps instead.
  • The plus and minus beside the size are for pointer users only: they are hidden from assistive technology, because the spin button already answers to Up and Down.
  • The color button's name joins two elements, "Text color" and the current color, so the choice is heard without opening the menu.
  • The color menu's items are menuitemradio with aria-checked, because exactly one color is chosen at a time.

Sources: WAI-ARIA Authoring Practices: Toolbar · WAI-ARIA Authoring Practices: Toolbar example · WAI-ARIA Authoring Practices: Spinbutton

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction