# Split Button

A main action button with a second button that opens a menu of alternatives.

```tsx
import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, SplitButton, toast } from 'ferry-ui'
import { CalendarClock, Download, FileSpreadsheet, FileText } from 'lucide-react'

export default function SplitButtonHero() {
  return (
    <SplitButton
      icon={<Download />}
      onClick={() => toast.success('CSV export started')}
      menuLabel="More export options"
      menu={
        <>
          <DropdownMenuLabel>Export as</DropdownMenuLabel>
          <DropdownMenuItem onSelect={() => toast.success('Excel export started')}>
            <FileSpreadsheet /> Excel (.xlsx)
          </DropdownMenuItem>
          <DropdownMenuItem onSelect={() => toast.success('PDF export started')}>
            <FileText /> PDF
          </DropdownMenuItem>
          <DropdownMenuSeparator />
          <DropdownMenuItem onSelect={() => toast('Schedule an export')}>
            <CalendarClock /> Schedule an export…
          </DropdownMenuItem>
        </>
      }
    >
      Export CSV
    </SplitButton>
  )
}
```

## Usage guidelines

- **One default action and a few alternatives.** The main button runs the default action. The menu holds variants of this action.
- **Equal actions are separate buttons.** If no action is the default, use two [Button](/docs/components/button) components.
- **A menu alone is not a split button.** For a button that only opens a menu, or for unrelated actions, use [Dropdown Menu](/docs/components/dropdown-menu).
- **Confirm a destructive item.** Open a [Confirm Dialog](/docs/components/confirm-dialog) from `onSelect` of the item.

## Anatomy

Import the component. Give the label as the child and the items in `menu`.

```tsx title="Anatomy"

<SplitButton
  onClick={() => {}}
  menuLabel=""
  menu={
    <>
      <DropdownMenuItem onSelect={() => {}} />
    </>
  }
>
  {/* the label of the main action */}
</SplitButton>
```

The `menu` prop takes the items, the labels, the separators and the groups of Dropdown Menu. Run each action in `onSelect` of its item.

## Examples

### Variants

The `variant` prop sets the look of the two buttons. A split button has five of the variants of Button.

```tsx
import { DropdownMenuItem, SplitButton } from 'ferry-ui'

const VARIANTS = [
  { variant: 'default', label: 'Default' },
  { variant: 'primary', label: 'Primary' },
  { variant: 'outline', label: 'Outline' },
  { variant: 'destructive', label: 'Destructive' },
  { variant: 'warning', label: 'Warning' },
] as const

export default function SplitButtonVariants() {
  return (
    <>
      {VARIANTS.map(({ variant, label }) => (
        <SplitButton
          key={variant}
          variant={variant}
          menuLabel={`More actions, ${variant}`}
          menu={
            <>
              <DropdownMenuItem>First alternative</DropdownMenuItem>
              <DropdownMenuItem>Second alternative</DropdownMenuItem>
            </>
          }
        >
          {label}
        </SplitButton>
      ))}
    </>
  )
}
```

### Sizes

The `size` prop sets the height of the two buttons.

```tsx
import { DropdownMenuItem, SplitButton } from 'ferry-ui'

const SIZES = [
  { size: 'tiny', label: 'Tiny' },
  { size: 'sm', label: 'Small' },
  { size: 'md', label: 'Medium' },
  { size: 'lg', label: 'Large' },
] as const

export default function SplitButtonSizes() {
  return (
    <>
      {SIZES.map(({ size, label }) => (
        <SplitButton
          key={size}
          size={size}
          menuLabel={`More actions, ${size}`}
          menu={
            <>
              <DropdownMenuItem>First alternative</DropdownMenuItem>
              <DropdownMenuItem>Second alternative</DropdownMenuItem>
            </>
          }
        >
          {label}
        </SplitButton>
      ))}
    </>
  )
}
```

| Size | Height |
| --- | --- |
| `tiny` | 26px |
| `sm` | 30px (default) |
| `md` | 34px |
| `lg` | 38px |

### Loading state

Set `loading` while the main action is in progress. The main button shows a spinner. The two buttons are disabled.

```tsx
import * as React from 'react'
import { DropdownMenuItem, SplitButton, toast } from 'ferry-ui'
import { CalendarClock, Save, Send } from 'lucide-react'

export default function SplitButtonLoading() {
  const [publishing, setPublishing] = React.useState(false)

  function publish() {
    setPublishing(true)
    // Stands for a request to the server.
    window.setTimeout(() => {
      setPublishing(false)
      toast.success('Page published')
    }, 1500)
  }

  return (
    <SplitButton
      variant="primary"
      icon={<Send />}
      loading={publishing}
      onClick={publish}
      menuLabel="More publish options"
      menu={
        <>
          <DropdownMenuItem onSelect={() => toast('Schedule the page')}>
            <CalendarClock /> Schedule for later…
          </DropdownMenuItem>
          <DropdownMenuItem onSelect={() => toast.success('Draft saved')}>
            <Save /> Save as draft
          </DropdownMenuItem>
        </>
      }
    >
      {publishing ? 'Publishing…' : 'Publish'}
    </SplitButton>
  )
}
```

### Menu that stays available

`disabled` disables the two buttons. If the alternatives still apply, set `menuDisabled` to `false`. Tell the user why the main action is not available.

```tsx
import { DropdownMenuItem, SplitButton, toast } from 'ferry-ui'
import { Download, Link2, Mail } from 'lucide-react'

export default function SplitButtonMenuAvailable() {
  return (
    <>
      <p id="send-invoice-reason" className="text-[13px] text-foreground-light">
        Add a billing email to send this invoice.
      </p>
      <SplitButton
        icon={<Mail />}
        disabled
        // The main action is not available, but its alternatives are.
        menuDisabled={false}
        menuLabel="More invoice actions"
        actionProps={{ 'aria-describedby': 'send-invoice-reason' }}
        menu={
          <>
            <DropdownMenuItem onSelect={() => toast.success('PDF download started')}>
              <Download /> Download PDF
            </DropdownMenuItem>
            <DropdownMenuItem onSelect={() => toast.success('Payment link copied')}>
              <Link2 /> Copy payment link
            </DropdownMenuItem>
          </>
        }
      >
        Send invoice
      </SplitButton>
    </>
  )
}
```

### Controlled menu

To open the menu from the code, pass `open` and `onOpenChange`. Update the state in `onOpenChange`, or the menu cannot close.

```tsx
import * as React from 'react'
import { Button, DropdownMenuItem, SplitButton, toast } from 'ferry-ui'

export default function SplitButtonControlled() {
  const [open, setOpen] = React.useState(false)

  return (
    <>
      <Button variant="ghost" onClick={() => setOpen(true)}>
        Show the other formats
      </Button>
      <SplitButton
        open={open}
        // Always update the state here, or the menu cannot close.
        onOpenChange={setOpen}
        onClick={() => toast.success('CSV export started')}
        menuLabel="More export options"
        menu={
          <>
            <DropdownMenuItem onSelect={() => toast.success('Excel export started')}>Excel (.xlsx)</DropdownMenuItem>
            <DropdownMenuItem onSelect={() => toast.success('PDF export started')}>PDF</DropdownMenuItem>
          </>
        }
      >
        Export CSV
      </SplitButton>
    </>
  )
}
```

### Submit button

With `type="submit"`, the main button submits the form around it. <Kbd>Enter</Kbd> in a field then runs the main action.

```tsx
import { Button, DropdownMenuItem, Field, Input, SplitButton, toast } from 'ferry-ui'
import { CalendarClock, Save } from 'lucide-react'

export default function SplitButtonSubmit() {
  return (
    <form
      className="mx-auto flex w-full max-w-sm flex-col gap-4"
      onSubmit={(event) => {
        event.preventDefault()
        toast.success('Page published')
      }}
    >
      <Field label="Title">
        <Input defaultValue="Release notes for March" />
      </Field>
      <div className="flex items-center justify-end gap-2">
        <Button type="reset">Cancel</Button>
        {/* Enter in the field submits the form: it runs the main action. */}
        <SplitButton
          type="submit"
          variant="primary"
          menuLabel="More publish options"
          menu={
            <>
              <DropdownMenuItem onSelect={() => toast('Schedule the page')}>
                <CalendarClock /> Schedule for later…
              </DropdownMenuItem>
              <DropdownMenuItem onSelect={() => toast.success('Draft saved')}>
                <Save /> Save as draft
              </DropdownMenuItem>
            </>
          }
        >
          Publish
        </SplitButton>
      </div>
    </form>
  )
}
```

### Destructive item

If the user cannot undo the action of an item, give the item `variant="destructive"`. Open the confirmation from its `onSelect`. A `className` in `menuProps` sets the width of the menu.

```tsx
import * as React from 'react'
import { ConfirmDialog, DropdownMenuItem, DropdownMenuSeparator, SplitButton, toast } from 'ferry-ui'
import { Ban, RefreshCw } from 'lucide-react'

export default function SplitButtonDestructive() {
  const [open, setOpen] = React.useState(false)
  const [all, setAll] = React.useState(false)

  function ask(allKeys: boolean) {
    setAll(allKeys)
    setOpen(true)
  }

  return (
    <>
      <SplitButton
        variant="destructive"
        onClick={() => ask(false)}
        menuLabel="More revoke options"
        menuProps={{ className: 'w-64' }}
        menu={
          <>
            <DropdownMenuItem onSelect={() => toast.success('Key replaced')}>
              <RefreshCw /> Revoke and create a replacement
            </DropdownMenuItem>
            <DropdownMenuSeparator />
            <DropdownMenuItem variant="destructive" onSelect={() => ask(true)}>
              <Ban /> Revoke all API keys…
            </DropdownMenuItem>
          </>
        }
      >
        Revoke key
      </SplitButton>
      <ConfirmDialog
        open={open}
        onOpenChange={setOpen}
        title={all ? 'Revoke all API keys?' : 'Revoke API key “Analytics export”?'}
        description="Requests that use a revoked key start to fail immediately. You cannot undo this."
        confirmLabel={all ? 'Revoke all keys' : 'Revoke key'}
        onConfirm={() => {
          toast.success(all ? 'All API keys revoked' : 'API key revoked')
        }}
      />
    </>
  )
}
```

## Accessibility

- `menuLabel` is the accessible name of the menu button. The default is "More actions".
- If a view has more than one split button, name the action in `menuLabel`: "More publish options".
- <Kbd>Enter</Kbd> on the menu button opens the menu. The arrow keys move between the items.

## API reference

`SplitButton` also accepts each attribute of the `<div>` element. They go to the root group.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` |  | Label of the main action button. Name the action ("Publish", "Export CSV"). |
| `menu` (required) | `ReactNode` |  | The menu opened by the chevron: DropdownMenuItem / Label / Separator / Group elements (run each action in the item's `onSelect`). Keep it to alternatives of the main action. |
| `onClick` | `MouseEventHandler<HTMLButtonElement>` |  | Runs the main action when the left button is clicked. |
| `menuLabel` | `string` | `More actions` | Accessible name of the icon-only chevron trigger. Default "More actions"; name the main action ("More publish options") when several split buttons share a screen. |
| `variant` | `"default" \| "primary" \| "outline" \| "destructive" \| "warning"` | `default` | Button style of both halves. Default `default`. |
| `size` | `"tiny" \| "sm" \| "md" \| "lg"` | `sm` | Height of both halves. Default `sm` (30px). |
| `icon` | `ReactNode` |  | Leading icon of the main button (replaced by a spinner while `loading`). |
| `loading` | `boolean` | `false` | The main action is in flight: spinner on the main button, both halves disabled (see `menuDisabled`). |
| `disabled` | `boolean` | `false` | Disables the main button, and the menu trigger unless `menuDisabled` says otherwise. |
| `menuDisabled` | `boolean` |  | Disables the menu trigger on its own. Defaults to `disabled \|\| loading`; pass `false` to keep the menu reachable while the main action is disabled or running (e.g. its alternatives still apply). |
| `type` | `"button" \| "submit" \| "reset"` | `button` | Native type of the main button. Default `button`; use `submit` to submit the enclosing form. |
| `open` | `boolean` |  | Controlled open state of the menu. |
| `defaultOpen` | `boolean` |  | Initial open state of the menu when uncontrolled. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called when the menu opens or closes. |
| `menuProps` | `SplitButtonMenuProps` |  | Props of the menu panel: `align` (default `end`, flush with the chevron), `side`, `className` (set a width such as `w-60` for long items)… |
| `actionProps` | `SplitButtonActionProps` |  | Extra props for the main button (`title`, `name`, `form`, `aria-describedby`, `className`…). |
