caliper uidocs

Split button

Free

A primary action joined to its alternatives; the chosen option becomes the label.

Installs components/ui/split-button.tsxDepends on @radix-ui/react-dropdown-menu, @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/split-button.json

Ready to merge · 4 commits become one on main.

When to use #

Use it for

  • One action is the usual choice and a few variants sit beside it, like Merge with Squash and Rebase.
  • Export or publish flows where the default format is obvious and the rest are one click away.

Skip it when

  • There is no default action; use a dropdown menu so every option is equal.
  • There are no alternatives; use a plain button.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/split-button.json

Adds its 5 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { GitMerge } from "lucide-react"
import { useState } from "react"
import { SplitButton } from "@/components/ui/split-button"

export function MergeButton() {
  const [status, setStatus] = useState("Ready to merge · 4 commits become one on main.");
  return (
    <div className="grid justify-items-center gap-3">
      <SplitButton
        menuLabel="Choose merge method"
        menuHeading="Merge method · remembered for halden-web"
        icon={<GitMerge aria-hidden="true" />}
        defaultValue="squash"
        options={[
          { value: "squash", label: "Squash and merge", description: "4 commits become one on main" },
          { value: "merge", label: "Create a merge commit", description: "Keeps all 4 commits and adds a merge" },
          { value: "rebase", label: "Rebase and merge", description: "Unavailable: 2 files conflict with main", disabled: true },
        ]}
        onValueChange={(value) => setStatus(value === "merge" ? "Ready to merge · keeps all 4 commits." : "Ready to merge · 4 commits become one on main.")}
        onRun={(value) => setStatus(`Merged #2290 into main with ${value === "merge" ? "a merge commit" : "squash"} at 14:32.`)}
      />
      <p role="status" className="text-sm text-muted-foreground">{status}</p>
    </div>
  );
}

API reference #

SplitButton

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
variant"default" | "secondary" | "ghost" | "destructive"—
asChildboolean—
loadingboolean—Shows a spinner over the label and swallows clicks while keeping focus and width.
feedbackKeystring—Animate a meaningful label/icon change, such as saving → saved.
menuLabelstring—
size"sm" | "md" | "lg"—
actionsreadonly SplitAction[]—Alternatives that run immediately from the menu.
optionsreadonly SplitOption[]—Choices; the selected one becomes the button's label.

Accessibility #

It is built on native elements and keeps aria-label and aria-hidden in step with state.

Keyboard and focus handling comes from Radix dropdown-menu: arrow keys move through items, Enter and Space select, and Escape closes and returns focus.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.