Propel

Split Button

A main action button paired with a chevron that opens a menu of secondary actions.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem } from "@makeplane/propel/components/menu";
import { SplitButton } from "@makeplane/propel/components/split-button";
import { Plus } from "lucide-react";

export default function BasicDemo() {
  return (
    <Menu>
      <SplitButton
        variant="primary"
        size="md"
        label="Create work item"
        icon={<Icon icon={Plus} />}
      />
      <MenuContent align="end">
        <MenuItem label="Create from template" />
        <MenuItem label="Import work items" />
      </MenuContent>
    </Menu>
  );
}

Installation

import { SplitButton } from "@makeplane/propel/components/split-button";

Usage

SplitButton ships no menu of its own — render it as a Menu’s child, with the surface as a sibling MenuContent. Clicking the main segment fires onClick; the chevron opens the menu.

import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem } from "@makeplane/propel/components/menu";
import { SplitButton } from "@makeplane/propel/components/split-button";
import { Plus } from "lucide-react";

export default function BasicDemo() {
  return (
    <Menu>
      <SplitButton
        variant="primary"
        size="md"
        label="Create work item"
        icon={<Icon icon={Plus} />}
      />
      <MenuContent align="end">
        <MenuItem label="Create from template" />
        <MenuItem label="Import work items" />
      </MenuContent>
    </Menu>
  );
}

Examples

Variants

Primary lays the segments out as separate pills; secondary connects them with a divider. There is no tertiary or ghost split button, and no danger palette — both segments are neutral.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem } from "@makeplane/propel/components/menu";
import { SplitButton } from "@makeplane/propel/components/split-button";
import { Plus } from "lucide-react";

export default function VariantsDemo() {
  return (
    <div className="flex flex-wrap items-center gap-6">
      {(["primary", "secondary"] as const).map((variant) => (
        <Menu key={variant}>
          <SplitButton
            variant={variant}
            size="md"
            label="Button"
            icon={<Icon icon={Plus} />}
            menuLabel={`More options (${variant})`}
          />
          <MenuContent align="end">
            <MenuItem label="Secondary action" />
          </MenuContent>
        </Menu>
      ))}
    </div>
  );
}

Sizes

One shared size sizes both segments — sm 24 / md 28 / lg 32 px. There is no xs: the segments are contiguous, so the 20px rung’s synthesized pointer target would overlap its neighbour. See Sizing for the full ladder.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem } from "@makeplane/propel/components/menu";
import { SplitButton } from "@makeplane/propel/components/split-button";
import { Plus } from "lucide-react";

export default function SizesDemo() {
  return (
    <div className="flex flex-col items-start gap-6">
      {(["sm", "md", "lg"] as const).map((size) => (
        <Menu key={size}>
          <SplitButton
            variant="primary"
            size={size}
            label="Button"
            icon={<Icon icon={Plus} />}
            menuLabel={`More options (${size})`}
          />
          <MenuContent align="end">
            <MenuItem label="Secondary action" />
          </MenuContent>
        </Menu>
      ))}
    </div>
  );
}

Loading

loading shows a spinner on the main segment (soft-disabled: still focusable, aria-busy) and disables the menu segment so the pending action can’t fork.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem } from "@makeplane/propel/components/menu";
import { SplitButton } from "@makeplane/propel/components/split-button";
import { Plus } from "lucide-react";

export default function LoadingDemo() {
  return (
    <Menu>
      <SplitButton
        variant="primary"
        size="md"
        label="Create work item"
        icon={<Icon icon={Plus} />}
        loading
      />
      <MenuContent align="end">
        <MenuItem label="Create from template" />
      </MenuContent>
    </Menu>
  );
}

Disabled

disabled disables both segments.

Show code
import { Icon } from "@makeplane/propel/components/icon";
import { Menu, MenuContent, MenuItem } from "@makeplane/propel/components/menu";
import { SplitButton } from "@makeplane/propel/components/split-button";
import { Plus } from "lucide-react";

export default function DisabledDemo() {
  return (
    <Menu>
      <SplitButton
        variant="primary"
        size="md"
        label="Create work item"
        icon={<Icon icon={Plus} />}
        disabled
      />
      <MenuContent align="end">
        <MenuItem label="Create from template" />
      </MenuContent>
    </Menu>
  );
}

API Reference

SplitButton

The ready-made split button: a main action `Button` plus a chevron `IconButton` grafted as the surrounding menu's `MenuTrigger`, laid out by the styled `SplitButton` frame (separate pills when primary, connected outline when secondary). Neutral only — there is no danger/error split button. It ships no menu of its own — render it as a `Menu`'s child, with the surface as a sibling `MenuContent`: ```tsx <Menu> <SplitButton variant="primary" size="md" label="Save" onClick={save} /> <MenuContent> <MenuItem label="Save as draft" /> </MenuContent> </Menu>; ```

PropTypeDefaultDescription
variant(required)"primary" | "secondary"
size(required)"sm" | "md" | "lg"
label(required)stringVisible label of the main action segment.
onClickMouseEventHandler<HTMLDivElement> | MouseEventHandler<HTMLButtonElement>Click handler of the main action segment.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.
iconReactNodeIcon rendered beside the main label (inline-start by default), e.g. `<Icon icon={Plus} />`.
iconPosition"start" | "end""start"Which side of the main label the icon sits on. The `loading` spinner takes the same slot.
loadingbooleanfalseShows a spinner on the main segment and makes both segments non-interactive (the main segment stays focusable, Button's soft-disabled loading state).
disabledbooleanDisables both segments.
type"button" | "submit" | "reset"buttonThe main segment's form behavior.
menuLabelstringMore optionsAccessible name of the menu-opening segment (it is icon-only, so it must be labeled).