Skip to content

Actions

Split Button

A default-action button paired with a menu of alternatives.

When to use it: Use it when one action is the common choice but a few related ones should stay one reach away.

On this page

Example

Loading example…
Split Button.tsxtsx
import {
  Button,
  Menu,
  MenuItem,
  MenuList,
  MenuPopover,
  MenuTrigger,
  SplitButton,
} from "@comp0/react";
import { ChevronDownIcon } from "@heroicons/react/16/solid";

export function Example() {
  const save = () => {};
  const saveAs = () => {};
  const saveCopy = () => {};

  return (
    <SplitButton aria-label="Save" className="inline-flex items-stretch rounded-md shadow-sm">
      <Button
        onClick={save}
        className="rounded-l-md bg-teal-600 px-3 py-2.5 text-base text-white data-hovered:bg-teal-700 data-focus-visible:outline data-focus-visible:-outline-offset-2 data-focus-visible:outline-2 data-focus-visible:outline-white sm:py-2 sm:text-sm dark:bg-teal-500 dark:text-teal-950 dark:data-hovered:bg-teal-400"
      >
        Save
      </Button>
      <Menu>
        <MenuTrigger
          aria-label="More save options"
          className="flex items-center rounded-r-md border-l border-white/30 bg-teal-600 px-2 text-white hover:bg-teal-700 focus-visible:outline focus-visible:-outline-offset-2 focus-visible:outline-2 focus-visible:outline-white dark:border-teal-950/20 dark:bg-teal-500 dark:text-teal-950 dark:hover:bg-teal-400"
        >
          <ChevronDownIcon className="size-4" aria-hidden="true" />
        </MenuTrigger>
        <MenuPopover
          placement="bottom end"
          offset={8}
          className="w-44 rounded border-0 bg-white p-1 shadow-lg ring-1 ring-zinc-950/10 dark:bg-zinc-900 dark:shadow-none dark:ring-white/10"
        >
          <MenuList>
            <MenuItem
              onClick={saveAs}
              value="save-as"
              className="w-full rounded px-3 py-2.5 text-left text-base text-zinc-800 select-none hover:bg-zinc-100 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800"
            >
              Save as…
            </MenuItem>
            <MenuItem
              onClick={saveCopy}
              value="save-copy"
              className="w-full rounded px-3 py-2.5 text-left text-base text-zinc-800 select-none hover:bg-zinc-100 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800"
            >
              Save a copy…
            </MenuItem>
          </MenuList>
        </MenuPopover>
      </Menu>
    </SplitButton>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. SplitButton

    Groups the default button and the menu button as one tab stop; arrow keys move between them. Owns a DOM element.

  2. Button

    The default action; disabling it drops it from the arrow-key order. Owns a DOM element.

  3. MenuTrigger

    The menu button that opens the alternatives. Owns a DOM element.

  4. MenuPopover / MenuList / MenuItem

    The floating surface, action list, and alternative actions from the Menu family. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Wrap a Button and a Menu in SplitButton and give it an aria-label.

  2. 2

    Add the supporting parts

    Put the default action on the Button; give the MenuTrigger its own name such as More options.

  3. 3

    Make the behavior clear

    Arrow keys move between the two segments as one tab stop; the menu opens from its own button.

    Exampletsx
    <SplitButton aria-label="Save">
      <Button onClick={save}>Save</Button>
      <Menu>
        <MenuTrigger aria-label="More save options">▾</MenuTrigger>
        <MenuPopover>
          <MenuList>
            <MenuItem onClick={saveAs}>Save as…</MenuItem>
          </MenuList>
        </MenuPopover>
      </Menu>
    </SplitButton>;

Keyboard

⇥
Moves into the control at the last-used segment; Tab again leaves.
→
Moves to the next segment without wrapping.
←
Moves to the previous segment without wrapping.
Home
Moves to the first segment.
End
Moves to the last segment.
↓↑
Opens the menu from the menu button. · menu button
↵
Presses the focused segment.
Space
Presses the focused segment.

Forms and accessibility

A split button does not create form values; its buttons submit their own.

Accessibility checklist

  • Give SplitButton an aria-label and the menu button its own distinct name.
  • Keep the default action first so it reads and roves before the menu button.
  • Offer the menu's actions somewhere else too; a hidden menu is easy to miss.

API reference

Importtsx
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@comp0/react";

SplitButton

DOM element

Groups the default button and the menu button as one tab stop; arrow keys move between them.

PropTypeDescription
aria-labelstringNames the whole control for assistive technology.

Button

DOM element

The default action; disabling it drops it from the arrow-key order.

PropTypeDescription
onClick(event) => voidRuns the default action.
disabledbooleanRemoves this segment from the tab stop.

MenuTrigger

DOM element

The menu button that opens the alternatives.

PropTypeDescription
aria-labelstringNames the menu segment, distinct from the default action.

MenuPopover / MenuList / MenuItem

DOM element

The floating surface, action list, and alternative actions from the Menu family.

Keep exploring