Skip to content
WANCRAFT

A.153Buttons & Controls

Split Button

A primary action joined to a chevron menu of alternatives, with descriptions, typeahead and an optional remember-last-action mode.

NewReactMotionTailwindintermediate

Overview

Split Button pairs the action people take most with the ones they take sometimes. The left half runs the primary action; the chevron is a proper menu button that opens a list of alternatives with icons, one-line descriptions and disabled states. The menu lives in the top layer, lines up with either edge of the button, flips above it when there is no room below and stays inside the viewport while the page scrolls. Turn on rememberLast and whatever you pick from the menu runs and becomes the new primary action, with the label rolling into place. Arrow keys, Home / End, typeahead, Enter / Space and Escape behave like a native menu, and focus always lands back on the chevron.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/split-button.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/split-button.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/split-button.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { SplitButton, type SplitButtonAction } from "@/components/ui/split-button";

export function SplitButtonExample() {
  const actions: SplitButtonAction[] = [
    { id: "merge", label: "Merge pull request", onSelect: () => console.log("merge") },
    { id: "squash", label: "Squash and merge", description: "Combine all commits into one." },
    { id: "rebase", label: "Rebase and merge", description: "Replay commits onto the base branch." },
    { id: "auto", label: "Enable auto-merge", description: "Requires branch protection.", disabled: true },
  ];

  return (
    <SplitButton actions={actions} onAction={(id) => console.log(id)} />
  );
}

03Props

PropTypeDefaultDescription
actionsSplitButtonAction[]—Required. { id, label, description?, icon?, disabled?, onSelect? }. The primary one sits on the button, the rest in the menu.
primaryIdstring—Controlled id of the primary action.
defaultPrimaryIdstring—Initial primary action when uncontrolled (defaults to the first action).
onPrimaryChange(id: string) => void—Called when a menu choice becomes the new primary (rememberLast).
onAction(id: string) => void—Called with the id of every triggered action.
rememberLastbooleantrueMake the action chosen from the menu the new primary action.
variant"solid" | "outline""solid"Filled with the accent, or a neutral outlined surface.
size"sm" | "md" | "lg""md"Height 32 / 40 / 48px.
showDescriptionsbooleantrueShow each action's description in the menu (also wired as aria-describedby).
menuWidthnumber260Minimum menu width in px.
align"start" | "end""end"Which edge of the button the menu lines up with.
spring{ stiffness; damping; mass }{ 560, 34, 0.7 }Spring for the menu scale-in.
accentstring"#FF7448"Solid fill, highlighted item tint and focus rings.
menuLabelstring"More actions"Accessible name of the chevron button and its menu.
disabledbooleanfalseDisable both halves.
...propsHTMLAttributes<HTMLDivElement>—Forwarded to the group element (className, aria-label…).

04Features

  • 01Primary action and chevron menu in one control, solid or outline, three sizes
  • 02Remember-last-action mode: a menu choice runs and becomes the primary, its label rolling into place
  • 03Menu items with icons, one-line descriptions and disabled states
  • 04Top-layer menu (Popover API) aligned to either edge, flipping above the button and clamped to the viewport
  • 05Re-positions while the page scrolls; outside clicks and window blur close it
motion · springinput · clickinput · keyboardinput · hover

05Accessibility

  • The two halves are real buttons inside role="group"; the chevron has an accessible name, aria-haspopup="menu", aria-expanded and aria-controls.
  • The menu is role="menu" labelled by the chevron, with role="menuitem" items, aria-disabled on disabled items and descriptions exposed through aria-describedby.
  • Keyboard: Enter, Space or ArrowDown on the chevron opens the menu on the first item and ArrowUp on the last; inside it Arrow keys, Home / End and typeahead move focus, Enter / Space run the item, and Escape or Tab close it and return focus to the chevron.
  • Visible focus rings in the accent colour; icons are aria-hidden; with prefers-reduced-motion the menu and label only fade.

06Limitations

  • Without Popover API support the menu falls back to position: fixed, which transformed ancestors can offset.