Morpha UI

Split Button

A primary action joined to a trailing chevron that opens a menu of secondary actions.

Overview

A split button pairs a primary action with a small trailing chevron that opens a menu of related, secondary actions. Splitting the two keeps the main action one click away while tucking the extra options behind the menu — reducing visual complexity without hiding them entirely. Reach for it when one action dominates but a few close alternatives deserve a home (Save / Save as… / Save a copy).

It is a composable shell built from three parts: SplitButton sets the shared style and groups the two halves with a 2px gap, SplitButtonAction is the leading primary button, and SplitButtonTrigger is the trailing chevron. It shares the Button's geometry, state-layer model and focus ring, and the same five styles — filled (default), tonal, outlined, elevated and glass. Outer corners follow the shape; the two inner facing corners stay small (and grow with size) for a harmonious nested look.

The trigger is menu-agnostic: it defaults aria-haspopup="menu" and supports the render prop, so you can hand it to a Base UI Menu/Popover trigger, or drive it yourself with aria-expanded + onClick. Either way it holds its state layer and rotates the chevron 180° while the menu is open.

$ npx morpha-ui add split-button

Examples

Variants

The five styles match the rest of the button family. Both halves always share one style. glass is translucent and is meant to sit over imagery or a colored backdrop.

Sizes

Five sizes scale padding, label and icon together — xs (32px), sm (40px, default), md (56px), lg (96px) and xl (136px). xs/sm keep a 44×44px hit target on each half for accessibility.

Radius

shape controls the outer corners — full (default), rounded (12px), squared (a per-size radius that grows with the button) and none. The two inner facing corners stay small so the halves read as one split control.

Usage

The trailing trigger is a shell — bring your own menu. The cleanest path is to hand it to a menu trigger via render; the trigger reflects data-popup-open, which holds the state layer and rotates the chevron (no extra wiring):

'use client';

import { Save } from 'lucide-react';
import { Menu } from '@base-ui/react/menu';
import {
  SplitButton,
  SplitButtonAction,
  SplitButtonTrigger,
} from '@/components/ui/split-button';

export function Example() {
  return (
    <SplitButton aria-label="Save options">
      <SplitButtonAction onClick={onSave}>
        <Save />
        Save
      </SplitButtonAction>

      <Menu.Root>
        <Menu.Trigger
          render={<SplitButtonTrigger aria-label="More save options" />}
        />
        <Menu.Portal>
          <Menu.Positioner align="end" sideOffset={8}>
            <Menu.Popup>{/* …items… */}</Menu.Popup>
          </Menu.Positioner>
        </Menu.Portal>
      </Menu.Root>
    </SplitButton>
  );
}

Prefer to control it yourself? Wire aria-expanded + onClick on the trigger instead and render your own menu:

<SplitButtonTrigger
  aria-label="More save options"
  aria-expanded={open}
  onClick={() => setOpen((v) => !v)}
/>

Installation

Morpha UI components are distributed via the Morpha CLI (coming in a later step): npx morpha-ui add split-button.

API Reference

Prop

Type

Prop

Type

Prop

Type

Accessibility

  • Renders two native <button>s grouped in a role="group" — give the group an aria-label describing the action set.
  • Provide an aria-label on SplitButtonTrigger — it is icon-only, so there is no visible text to name it. It defaults aria-haspopup="menu"; wire aria-expanded (and ideally aria-controls) to your menu.
  • A visible focus ring (3px accent with a 2px offset) is provided on each half via focus-visible.
  • prefers-reduced-motion keeps the state-layer/color transitions but drops the press scale and the chevron's animated rotation (the open state is still indicated).
  • loading on SplitButtonAction sets aria-busy and disables interaction while work is in flight; disabled on SplitButton disables both halves.

On this page