Accessibility pattern · Buttons and actions

Button

A real button element brings focus, Enter, Space and the button role with it; the work left is in its states. While it saves, it keeps focus and its width and says so in a status message; when it cannot be used, it stays focusable and says why.

WCAG criteria
7
Keyboard rules
4
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.

Variants

Sizes

With an icon

Loading

Unavailable

Add a cover image to publish.

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 through the buttons in order, including Publish while it is unavailable, so its reason can be heard.
Enter or SpacePresses the focused button.
Enter or SpaceOn Save draft, starts saving. Focus stays on the button, and presses are ignored until the save is done.
Enter or SpaceOn Publish while it is unavailable, changes nothing and repeats the reason in a status message.

Screen readers

What it announces

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

WhenExpected announcement
Focus reaches the danger buttonDelete, button
Save draft is pressedSaving
The save finishesDraft saved
Focus reaches Publish while it is unavailablePublish, button, unavailable. Add a cover image to publish.
Publish is pressed while unavailableNot published: add a cover image first.
A cover image is addedCover image added

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-button" data-ap-button>
  <div class="ap-button__row">
    <p class="ap-button__k">Variants</p>
    <div class="ap-button__set">
      <button type="button" class="ap-btn ap-btn--primary">Save changes</button>
      <button type="button" class="ap-btn">Preview</button>
      <button type="button" class="ap-btn ap-btn--ghost">Cancel</button>
      <button type="button" class="ap-btn ap-btn--danger">
        <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7h16"/><path d="M10 11v6M14 11v6"/><path d="m6 7 1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12"/><path d="M9 7V4h6v3"/></svg>
        Delete
      </button>
    </div>
  </div>

  <div class="ap-button__row">
    <p class="ap-button__k">Sizes</p>
    <div class="ap-button__set">
      <button type="button" class="ap-btn ap-button--sm">Small</button>
      <button type="button" class="ap-btn">Medium</button>
      <button type="button" class="ap-btn ap-button--lg">Large</button>
    </div>
  </div>

  <div class="ap-button__row">
    <p class="ap-button__k">With an icon</p>
    <div class="ap-button__set">
      <button type="button" class="ap-btn">
        <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4v11"/><path d="m7 10 5 5 5-5"/><path d="M5 20h14"/></svg>
        Download invoice
      </button>
      <button type="button" class="ap-btn ap-btn--ghost">
        Continue
        <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 12h14"/><path d="m13 6 6 6-6 6"/></svg>
      </button>
    </div>
  </div>

  <div class="ap-button__row">
    <p class="ap-button__k">Loading</p>
    <div class="ap-button__set">
      <button type="button" class="ap-btn ap-btn--primary ap-button__save" data-ap-save>
        <span class="ap-button__label">Save draft</span>
        <span class="ap-button__spin" aria-hidden="true"></span>
      </button>
      <p class="ap-button__status" role="status" data-ap-save-status>
        <svg class="ap-button__mark ap-button__mark--done" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
        <span class="ap-button__text" data-ap-text></span>
      </p>
    </div>
  </div>

  <div class="ap-button__row">
    <p class="ap-button__k">Unavailable</p>
    <div class="ap-button__stack">
      <div class="ap-button__set">
        <button type="button" class="ap-btn ap-btn--primary ap-button__publish" aria-disabled="true" aria-describedby="button-publish-why" data-ap-publish>
          <svg class="ap-btn__icon ap-button__lock" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>
          <svg class="ap-btn__icon ap-button__send" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg>
          Publish
        </button>
        <button type="button" class="ap-btn" data-ap-cover>
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="9" cy="10" r="2"/><path d="m21 16-5-5L5 20"/></svg>
          <span data-ap-cover-label>Add cover image</span>
        </button>
        <p class="ap-button__status" role="status" data-ap-publish-status>
          <svg class="ap-button__mark ap-button__mark--done" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
          <svg class="ap-button__mark ap-button__mark--warn" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4 2.5 20h19Z"/><path d="M12 10v4"/><path d="M12 17h.01"/></svg>
          <span class="ap-button__text" data-ap-text></span>
        </p>
      </div>
      <p class="ap-button__why" id="button-publish-why">
        <svg class="ap-button__mark ap-button__mark--info" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 8h.01"/></svg>
        <svg class="ap-button__mark ap-button__mark--done" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
        <span data-ap-why-text>Add a cover image to publish.</span>
      </p>
    </div>
  </div>
</div>

WCAG 2.2

What it meets

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

  • 1.4.1 Use of Color Level A

    The danger button says Delete and carries a bin icon, and the unavailable button shows a lock, so neither depends on its color.

  • 2.1.1 Keyboard Level A

    Every button is a native button element, so Enter and Space press it and Tab reaches it, with no extra script.

  • 2.4.7 Focus Visible Level AA

    Each button shows a two-pixel focus ring, offset from its edge so it reads on the filled variants too.

  • 2.5.3 Label in Name Level A

    Each name is the visible text, so a speech-input user can say "click Save draft" and it works.

  • 2.5.8 Target Size (Minimum) Level AA

    The smallest button is 32 pixels tall, above the 24-pixel minimum; the others are 40 and 48.

  • 4.1.2 Name, Role, Value Level A

    Icons are hidden from assistive technology, aria-disabled reports Publish as unavailable, and aria-busy marks the save in progress.

  • 4.1.3 Status Messages Level AA

    "Saving", "Draft saved" and the reason Publish is unavailable arrive in status messages, without moving focus.

Usage

When to use it

Use it

  • An action on the current page: saving, sending, opening a dialog, deleting.
  • Submitting a form; a submit button also lets Enter in a field send it.

Use something else

  • Going to another page or address: use a link, which can be opened in a new tab and bookmarked.
  • Switching a setting on and off: use a toggle button or a switch, which report their state.
  • Several related actions side by side: group them in a toolbar, or behind a menu button.

Common failures

How it usually goes wrong

  • A div or a link dressed as a button

    A div with a click handler gets no focus, no Enter or Space and no role. A link with href="#" is announced as a link and jumps the page. The button element does all of it with no script.

  • Disabled with no reason

    The disabled attribute takes a button out of the Tab order, so keyboard and screen reader users never learn it is there or why it does nothing. aria-disabled keeps it focusable, and a hint tied with aria-describedby says why.

  • Focus thrown away while loading

    Setting disabled during a save drops focus to the top of the page. Here the button keeps focus, ignores presses while it is busy, and a status message says what is happening.

  • A label that changes width

    Swapping the text for "Saving…" resizes the button, so the layout shifts under the pointer. The label stays where it is, made transparent, with a spinner over it.

  • Danger shown by color alone

    A red button that only says "OK" relies on its color to warn. The destructive button here says Delete and carries a bin icon.

  • Icons read aloud

    An SVG without aria-hidden can be announced as "image" or not at all, depending on the browser. Decorative icons are hidden; the text is the name.

Notes

Building it

  • Give every button type="button" unless it submits a form: the default type is submit, which sends the form the button sits in.
  • aria-disabled does not stop clicks, so the script checks it and returns early. That is the point: the press can explain itself instead of failing silently.
  • The busy label is faded with opacity, not visibility: hidden, because hidden text drops out of the accessible name and the focused button would lose its name.
  • Support for aria-busy varies between screen readers, so the status message carries the news; aria-busy is a hint on top.
  • The small size suits dense tables and toolbars. On touch screens keep primary actions at 44 pixels or more.

Sources: WAI-ARIA Authoring Practices: Button · HTML: the button element

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