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.
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>;
}
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.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| entriesrequired | readonly ActionMenuEntry[] | – | Ordered actions and choices. |
| onSelect | ((id: string) => void) | undefined | – | Reports an ordinary command; choices use their own change callbacks. |
| disabled | boolean | undefined | – | Disable the trigger, for example while an action is pending. |
| open | boolean | undefined | – | Controlled visibility. |
| defaultOpen | boolean | undefined | – | Initial visibility when uncontrolled. |
| onOpenChange | BaseMenu.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. |
| motion | boolean | undefined | – | Enable token-based entrance/exit; reduced motion removes scaling. |
| testId | string | undefined | – | Root trigger uses --trigger and popup uses --menu. |
| labelrequired | string | – | The primary button's visible label; the menu has its own name in menuLabel. |
| menuLabelrequired | string | – | Accessible name for the secondary menu button. |
| onClickrequired | () => void | – | Requests the primary action; the caller controls pending state through disabled. |
| variant | ButtonVariant | undefined | "default" | Visual emphasis for both buttons. |
| size | Exclude<ButtonSize, "icon"> | undefined | "default" | The primary button's size; the menu button keeps a square touch target. |
| textCase | ButtonTextCase | undefined | "none" | How the primary label is cased; as written by default. |
| icon | ReactNode | undefined | – | A decorative mark before the primary label, such as a Lucide icon with aria-hidden. |
| adornment | ReactNode | 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. |
| className | string | undefined | – | Additional wrapper classes. |
Related
- Action menu: menu entries and keyboard behavior.
- Button: emphasis and action labels.
- Interaction states: pending and disabled states.