Skip to content
design
Components

Split button

A primary action with a separately named menu of related alternatives.

On this page

This example requests a format without downloading a file.

Export in the primary format or open the adjacent menu for a different format.
Code
import { SplitButton } from "@bosunkit/components/split-button";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const title: string = "One primary export, with alternative formats";
export default function Example(): ReactElement {
  const [result,setResult]: [string,Dispatch<SetStateAction<string>>] = useState("This example requests a format without downloading a file.");
  return <div><SplitButton label="Export PDF" menuLabel="Other export formats" variant="primary" onClick={(): void => { setResult("PDF export requested."); }}
    entries={[{id:"markdown",label:"Export Markdown"},{id:"text",label:"Export plain text"}]}
    onSelect={(id:string): void => { setResult(id === "markdown" ? "Markdown export requested." : "Plain-text export requested."); }} />
    <p role="status">{result}</p></div>;
}
01A clear primary actionThe large button states exactly what pressing it will do. The adjacent button opens alternatives.
02Two keyboard destinationsEach button has its own accessible name and target. Opening the menu never invokes the primary action.
03Related alternativesKeep the menu focused on variations of the same task. Use an ordinary action menu for unrelated commands.

Behavior

The primary button calls onClick; the menu reports command ids through onSelect. Choosing an alternative does not silently change the primary action. If the application remembers a preferred action, it must update both the visible label and its callback together.

menuLabel names the chevron button and its tooltip. Both buttons use the shared touch target role and retain their own focus indication. The menu inherits ActionMenu's grouping, checkbox and radio choices, submenus, placement and controlled visibility. Use variant="primary" only for the main task on the page.

Pending and disabled

The report is being prepared. Both buttons are disabled.

Code
import { SplitButton } from "@bosunkit/components/split-button";
import type { ReactElement } from "react";
export const title: string = "Both actions unavailable while the caller is busy";
function ignore(): void { /* Unavailable example. */ }
export default function Example(): ReactElement {
  return <div><SplitButton label="Preparing export…" menuLabel="Other export formats unavailable" disabled onClick={ignore}
    entries={[{id:"markdown",label:"Export Markdown"}]} /><p role="status">The report is being prepared. Both buttons are disabled.</p></div>;
}

The caller owns pending operations. Set disabled to block both buttons while necessary and show a truthful status beside the control. A failed request should retain the task's inputs and offer retry. Do not change a label to a completed result before the operation has succeeded.

Do
Keep the primary action and alternatives within one task.
Don't
Do not hide a destructive unrelated command beside an export.

Props

PropTypeDefaultDescription
entriesrequiredreadonly ActionMenuEntry[]–Ordered actions and choices.
onSelect((id: string) => void) | undefined–Reports an ordinary command; choices use their own change callbacks.
disabledboolean | undefined–Disable the trigger, for example while an action is pending.
openboolean | undefined–Controlled visibility.
defaultOpenboolean | undefined–Initial visibility when uncontrolled.
onOpenChangeBaseMenu.Root.Props["onOpenChange"]–Reports opening and dismissal.
side"top" | "bottom" | "left" | "right" | undefined–Preferred popup side; collision handling can flip it.
align"start" | "center" | "end" | undefined–Alignment along the trigger.
motionboolean | undefined–Enable token-based entrance/exit; reduced motion removes scaling.
testIdstring | undefined–Root trigger uses --trigger and popup uses --menu.
labelrequiredstring–The primary button's visible label; the menu has its own name in menuLabel.
menuLabelrequiredstring–Accessible name for the secondary menu button.
onClickrequired() => void–Requests the primary action; the caller controls pending state through disabled.
variantButtonVariant | undefined"default"Visual emphasis for both buttons.
sizeExclude<ButtonSize, "icon"> | undefined"default"The primary button's size; the menu button keeps a square touch target.
textCaseButtonTextCase | undefined"none"How the primary label is cased; as written by default.
iconReactNode | undefined–A decorative mark before the primary label, such as a Lucide icon with aria-hidden.
adornmentReactNode | undefined–Shown after the primary label and left out of its name, for a value that changes while the button is on the screen, such as a toast's seconds left; mark it aria-hidden.
classNamestring | undefined–Additional wrapper classes.