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

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