Accessibility pattern · Buttons and actions

Split button

A split button is two buttons that look like one: the main action does the common thing at once, and the arrow opens a menu of variations. Each half is its own tab stop with its own name, so nobody has to guess which half they are on.

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.

Draft

Monsoon recipes from Kerala

By Nikhil Menon · 6 min read

Rain on the backwaters, a pot of kappa on the stove, and five dishes that taste of June in Alappuzha.

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 between the two halves: Publish first, then the arrow.
Enter or SpaceOn Publish, publishes at once. On the arrow, opens the menu at its first option.
Arrow Down or Arrow UpOn the arrow, opens the menu at the first or last option; in the menu, moves between options, wrapping at the ends.
Home or EndIn the menu, moves to the first or last option.
A–ZIn the menu, moves to the next option starting with that letter.
Enter or SpaceIn the menu, runs the focused option, closes the menu and returns focus to the arrow.
EscapeCloses the menu and returns focus to the arrow. Tab, or a click outside, closes it too.

Screen readers

What it announces

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

WhenExpected announcement
Focus reaches the main halfPublish, button
Tab moves to the arrowMore publish options, menu button, collapsed
Enter opens the menuMore publish options, menu. Schedule…, menu item, 1 of 3
Escape closes itMore publish options, menu button, collapsed
Publish to drafts is chosenSaved to drafts

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-split-button" data-ap-split-button>
  <div class="ap-split-button__card">
    <div class="ap-split-button__body">
      <p class="ap-split-button__badge" data-state="draft"><span data-ap-badge>Draft</span></p>
      <h3 class="ap-split-button__name">Monsoon recipes from Kerala</h3>
      <p class="ap-split-button__meta">By Nikhil Menon · 6 min read</p>
      <p class="ap-split-button__excerpt">Rain on the backwaters, a pot of kappa on the stove, and five dishes that taste of June in Alappuzha.</p>
    </div>

    <div class="ap-split-button__foot">
      <p class="ap-split-button__status" role="status">
        <svg class="ap-split-button__done" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m5 12 5 5L20 7"/></svg>
        <span data-ap-text></span>
      </p>
      <div class="ap-split-button__group">
        <button type="button" class="ap-split-button__main" data-ap-main>
          <svg class="ap-split-button__icon" 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-split-button__arrow" id="split-button-arrow" aria-label="More publish options" aria-haspopup="menu" aria-expanded="false" aria-controls="split-button-menu">
          <svg class="ap-split-button__chev" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 15 6-6 6 6"/></svg>
        </button>
        <div class="ap-split-button__menu" role="menu" id="split-button-menu" aria-labelledby="split-button-arrow" hidden>
          <button type="button" class="ap-split-button__item" role="menuitem" tabindex="-1" data-ap-action="schedule">
            <svg class="ap-split-button__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>
            <span class="ap-split-button__label">Schedule…</span>
          </button>
          <button type="button" class="ap-split-button__item" role="menuitem" tabindex="-1" data-ap-action="drafts">
            <svg class="ap-split-button__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8Z"/><path d="M14 3v5h5"/><path d="M9 14h6M9 18h4"/></svg>
            <span class="ap-split-button__label">Publish to drafts</span>
          </button>
          <button type="button" class="ap-split-button__item" role="menuitem" tabindex="-1" data-ap-action="template">
            <svg class="ap-split-button__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>
            <span class="ap-split-button__label">Save as template</span>
          </button>
        </div>
      </div>
    </div>
  </div>
</div>

WCAG 2.2

What it meets

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

  • 1.4.11 Non-text Contrast Level AA

    The two halves are split by a visible gap, and each has its own focus ring, so it is clear which half has focus.

  • 2.1.1 Keyboard Level A

    Both halves, the menu and every option work from the keyboard.

  • 2.4.3 Focus Order Level A

    Focus enters the menu when it opens and returns to the arrow when it closes, so it is never lost.

  • 2.4.6 Headings and Labels Level AA

    The arrow is named "More publish options", not "More" or "Toggle", so its purpose is clear out of context.

  • 2.5.8 Target Size (Minimum) Level AA

    The arrow is a full 44-pixel target of its own, not a sliver of the main button.

  • 4.1.2 Name, Role, Value Level A

    The main half is a plain button; the arrow has aria-haspopup and aria-expanded and names the menu it opens.

  • 4.1.3 Status Messages Level AA

    Each result is confirmed in a status message, without taking focus from the button.

Usage

When to use it

Use it

  • One action people take most of the time, with a few variations of it: publish, publish later, save as draft.
  • Space for one button where the variations would otherwise need their own.

Use something else

  • Unrelated actions behind one arrow: use a separate menu button.
  • Variations that most people need: show them as separate buttons.
  • Choosing a setting that the main button then uses: a select beside a button is clearer.

Common failures

How it usually goes wrong

  • One button with two hit areas

    If the arrow is only a region inside a single button, keyboard and screen reader users can never reach the menu. Make it a real second button.

  • An arrow with no name

    An icon-only arrow announced as "button" tells nobody what it opens. aria-label names it after what it holds.

  • The main action hidden in the menu only

    Repeating the main action inside the menu is harmless, but leaving it out of the main half defeats the point of a split button.

  • Focus sent to the main half on close

    People who opened the menu with the arrow expect to land back on the arrow. Escape and every option return focus there.

  • A sliver of a target

    A 16-pixel arrow glued to a large button is easy to miss and easy to hit by accident. The arrow here is a 44-pixel square.

Notes

Building it

  • Keep the two halves visually joined but separated by a gap, so each focus ring reads as belonging to one half.
  • The menu is labelled by the arrow, so opening it announces "More publish options, menu".
  • An ellipsis after an option (Schedule…) promises a further step, like choosing a time; keep that promise in a real product.
  • The menu opens upwards here because the button sits at the bottom of its card; flip it to fit the space you have.

Sources: WAI-ARIA Authoring Practices: Menu Button · WAI-ARIA Authoring Practices: Button

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