# Split Button

> Primary action plus a chevron that opens a menu; closes on outside click or Escape.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/split-button
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/split-button.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/split-button-demo.tsx"
import { SplitButton } from "@/components/split-button"

export default function SplitButtonDemo() {
  return (
    <div className="flex min-h-56 flex-wrap items-start justify-center gap-6 p-8">
      <SplitButton />
      <SplitButton label="Merge" items={[{ label: "Squash and merge" }, { label: "Rebase and merge" }]} />
    </div>
  )
}
```

## Source

```tsx title="components/split-button.tsx"
"use client"

import * as React from "react"
import { ChevronDown } from "lucide-react"

import { cn } from "@/lib/utils"

export interface SplitButtonItem {
  label: string
  onSelect?: () => void
}

export interface SplitButtonProps extends Omit<React.ComponentProps<"button">, "children"> {
  label?: string
  items?: SplitButtonItem[]
}

const defaultItems: SplitButtonItem[] = [{ label: "Save as draft" }, { label: "Schedule" }, { label: "Save as template" }]

export function SplitButton({ label = "Publish", items = defaultItems, className, ...props }: SplitButtonProps) {
  const [open, setOpen] = React.useState(false)
  const rootRef = React.useRef<HTMLDivElement>(null)
  const menuId = React.useId()

  React.useEffect(() => {
    if (!open) return
    function onPointerDown(event: PointerEvent) {
      if (!rootRef.current?.contains(event.target as Node)) setOpen(false)
    }
    function onKeyDown(event: KeyboardEvent) {
      if (event.key === "Escape") setOpen(false)
    }
    document.addEventListener("pointerdown", onPointerDown)
    document.addEventListener("keydown", onKeyDown)
    return () => {
      document.removeEventListener("pointerdown", onPointerDown)
      document.removeEventListener("keydown", onKeyDown)
    }
  }, [open])

  const segment =
    "inline-flex h-10 items-center justify-center bg-primary text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"

  return (
    <div ref={rootRef} className="relative inline-flex">
      <div className="inline-flex rounded-lg shadow-xs">
        <button type="button" className={cn(segment, "rounded-l-lg px-5", className)} {...props}>
          {label}
        </button>
        <button
          type="button"
          aria-label="More options"
          aria-haspopup="menu"
          aria-expanded={open}
          aria-controls={open ? menuId : undefined}
          disabled={props.disabled}
          className={cn(segment, "rounded-r-lg border-l border-primary-foreground/25 px-2.5")}
          onClick={() => setOpen((value) => !value)}
        >
          <ChevronDown className={cn("size-4 transition-transform duration-200", open && "rotate-180")} />
        </button>
      </div>
      {open && (
        <ul
          id={menuId}
          role="menu"
          className="absolute top-full right-0 z-20 mt-2 min-w-44 overflow-hidden rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg"
        >
          {items.map((item) => (
            <li key={item.label} role="none">
              <button
                type="button"
                role="menuitem"
                className="flex w-full items-center rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:outline-none"
                onClick={() => {
                  item.onSelect?.()
                  setOpen(false)
                }}
              >
                {item.label}
              </button>
            </li>
          ))}
        </ul>
      )}
    </div>
  )
}
```
