ComponentsActions

Split button

A primary action with a caret beside it. The label runs the usual choice in one click; the caret opens the other ways to do the same thing.

Playground

Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.

States
import { SplitButton } from '@/ui/split-button';<SplitButton>  Send and remind me · 3 days</SplitButton>

Default

The reply composer: the label names the member's default reminder and one click sends with it. The caret opens the other reminder lengths above the button, with the default marked; each choice sends at once.

import { useState } from 'react';
import { SplitButton, SplitButtonAction, SplitButtonMenu, SplitButtonMenuItem } from '@/ui/split-button';
import { toast } from '@/ui/toast';

const reminders = [
  { label: 'Tomorrow', at: 'Fri 16 Oct, 09:00' },
  { label: 'Next week', at: 'Mon 19 Oct, 09:00' },
  { label: 'In 3 working days', at: 'Tue 20 Oct, 09:00', isDefault: true },
];

// The reply composer: the label runs the member's default reminder; the caret picks another. Each choice sends at once.
export default function SendAndRemind() {
  const [sending, setSending] = useState(false);
  const send = (when: string) => {
    if (sending) return;
    setSending(true);
    setTimeout(() => {
      setSending(false);
      toast.add({ type: 'success', title: `Sent to Maya Chen. Reminder ${when}.` });
    }, 900);
  };
  return (
    <SplitButton loading={sending}>
      <SplitButtonAction loadingLabel="Sending" onClick={() => send('Tue 20 Oct, 09:00')}>
        Send and remind me · 3 days
      </SplitButtonAction>
      <SplitButtonMenu
        label="Choose when to be reminded"
        title="Remind me if Maya hasn’t replied"
        footer="If no reply by then, it comes back to Needs reply with a nudge drafted. A reply cancels it. Weekends don’t count."
      >
        {reminders.map((r) => (
          <SplitButtonMenuItem key={r.label} detail={r.at} isDefault={r.isDefault} onClick={() => send(r.at)}>
            {r.label}
          </SplitButtonMenuItem>
        ))}
      </SplitButtonMenu>
    </SplitButton>
  );
}

States

Sending: the spinner holds the label's width and the caret waits. Disabled: both halves until there is a message to send, with the reason in a tooltip.

import { SplitButton, SplitButtonAction, SplitButtonMenu, SplitButtonMenuItem } from '@/ui/split-button';
import { Hint } from '@/ui/tooltip';

function Menu() {
  return (
    <SplitButtonMenu label="Choose when to be reminded">
      <SplitButtonMenuItem detail="Fri 16 Oct, 09:00">Tomorrow</SplitButtonMenuItem>
      <SplitButtonMenuItem detail="Tue 20 Oct, 09:00" isDefault>
        In 3 working days
      </SplitButtonMenuItem>
    </SplitButtonMenu>
  );
}

// Sending: the spinner holds the label's width and the caret waits. Disabled: both halves, until there is a message;
// the tooltip says why.
export default function States() {
  return (
    <div className="flex flex-wrap items-center gap-300">
      <SplitButton loading>
        <SplitButtonAction loadingLabel="Sending">Send and remind me · 3 days</SplitButtonAction>
        <Menu />
      </SplitButton>
      <SplitButton disabled>
        <Hint label="Write a message first">
          <SplitButtonAction>Send and remind me · 3 days</SplitButtonAction>
        </Hint>
        <Menu />
      </SplitButton>
    </div>
  );
}

Failed send and long labels

A send that fails keeps the draft and says what to do beside the button. A press while it sends does nothing, so a double click sends once. A long label stays on one line.

import { useState } from 'react';
import { InlineMessage } from '@/ui/inline-message';
import { SplitButton, SplitButtonAction, SplitButtonMenu, SplitButtonMenuItem } from '@/ui/split-button';

// A send that fails keeps the draft and says what to do beside the button. Pressing again while it sends does nothing.
export default function Failure() {
  const [sending, setSending] = useState(false);
  const [failed, setFailed] = useState(false);
  const send = () => {
    setSending(true);
    setFailed(false);
    setTimeout(() => {
      setSending(false);
      setFailed(true);
    }, 900);
  };
  return (
    <div className="flex flex-wrap items-center gap-150">
      <SplitButton loading={sending}>
        <SplitButtonAction loadingLabel="Sending" onClick={send}>
          Send to Nguyễn Thị Minh Khai and remind me · 3 days
        </SplitButtonAction>
        <SplitButtonMenu label="Choose when to be reminded">
          <SplitButtonMenuItem detail="Fri 16 Oct, 09:00" onClick={send}>
            Tomorrow
          </SplitButtonMenuItem>
        </SplitButtonMenu>
      </SplitButton>
      {failed && <InlineMessage>Couldn’t send. Your draft is kept; check your connection and try again.</InlineMessage>}
    </div>
  );
}

On this page