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 primaryActions are equally weighted
Secondary actions are rarely neededUsers need to see all options at a glance
Space is constrainedActions have different destructive risk levels
Deploy, export, or send patternsYou 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.

SplitButton.tsx
tsx
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

PropTypeDefaultDescription
labelstringLabel for the primary action button.
onClick() => voidHandler for the primary action.
itemsSplitButtonItem[]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.
leftIconReactNodeIcon rendered to the left of the label.
disabledbooleanfalseDisables both the action button and the chevron.
loadingbooleanfalseDisables interaction (loading state).

SplitButtonItem

PropTypeDefaultDescription
labelstringMenu item label.
iconReactNodeOptional icon rendered to the left.
onClick() => voidHandler called when the item is selected.
disabledbooleanGrays out and disables the item.
dangerbooleanRenders 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.

9:41

New post

ScenarioGuidance
Chevron tap targetThe 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 mobileIf 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 positioningOn 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 variantIn 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".