Split Button
A compound control that combines a primary action with a dropdown of secondary actions. The left segment triggers the default action immediately; the right chevron reveals the full menu.
Preview
When to use
A split button collapses a group of related actions into a single control when one action is clearly the default. It reduces visual clutter without hiding important secondary paths.
| Use a split button when… | Use separate buttons when… |
|---|---|
| One action is clearly primary | Actions are equally weighted |
| Secondary actions are rarely needed | Users need to see all options at a glance |
| Space is constrained | Actions have different destructive risk levels |
| Deploy, export, or send patterns | You have more than ~5 secondary actions |
Sizes
All three Button sizes. The chevron segment width scales proportionally.
Implementation
The dropdown is portaled to document.body via createPortal — it is never clipped by overflow: hidden ancestors. Position is computed from getBoundingClientRect on open. Click-outside dismissal is wired via a mousedown listener on document.
import { SplitButton } from "@/components/ui/SplitButton";
import { Download, FileText, Archive } from "lucide-react";
<SplitButton
label="Export"
variant="secondary"
leftIcon={<Download className="w-4 h-4" />}
onClick={() => exportDefault()}
items={[
{
label: "Export as CSV",
icon: <FileText className="w-3.5 h-3.5" />,
onClick: () => exportCSV(),
},
{
label: "Export as JSON",
icon: <FileText className="w-3.5 h-3.5" />,
onClick: () => exportJSON(),
},
{
label: "Delete export",
icon: <Archive className="w-3.5 h-3.5" />,
onClick: () => deleteExport(),
danger: true,
},
]}
/>SplitButton props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Label for the primary action button. |
onClick | () => void | — | Handler for the primary action. |
items | SplitButtonItem[] | — | Menu items shown in the dropdown. |
variant | "primary" | "secondary" | "ghost" | "danger" | "glass" | "primary" | Visual style — matches Button variants. |
size | "sm" | "md" | "lg" | "md" | Height and padding — matches Button sizes. |
leftIcon | ReactNode | — | Icon rendered to the left of the label. |
disabled | boolean | false | Disables both the action button and the chevron. |
loading | boolean | false | Disables interaction (loading state). |
SplitButtonItem
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Menu item label. |
icon | ReactNode | — | Optional icon rendered to the left. |
onClick | () => void | — | Handler called when the item is selected. |
disabled | boolean | — | Grays out and disables the item. |
danger | boolean | — | Renders the item in red — for destructive actions. |
Mobile
Split buttons have two small adjacent tap targets — the primary action and the chevron — which can be error-prone on touch. Adapt the layout for narrow screens.
New post
| Scenario | Guidance |
|---|---|
| Chevron tap target | The divider + chevron section should be at least 44px wide on mobile — wider than the default 32px. Consider a separate touch target area that expands beyond the visible chevron. |
| Simplify on mobile | If the secondary actions are rarely used, hide the split on mobile and show only the primary action. Put secondary actions in a separate overflow menu (⋯) in the toolbar. |
| Dropdown positioning | On mobile, the dropdown can be clipped by the bottom of the screen. Position it above the button when there is less than 200px of space below. |
| Full-width variant | In bottom action bars, a full-width split button works well: left ~80% is the primary action, right ~20% is the chevron. Divide with a 1px vertical separator. |
Accessibility
- →The chevron button has aria-haspopup="menu" and aria-expanded so screen readers announce it as a menu trigger.
- →The dropdown has role="menu"; each item has role="menuitem".
- →Keyboard: Enter/Space on the chevron opens the menu. Arrow keys move between items. Escape closes.
- →The primary action and the chevron are separate focusable elements — Tab visits them independently.
- →Danger items should describe what they do in plain language, not just label them "Delete".