Button Group
@cloudflare/kumo
import { Button, ButtonGroup, DropdownMenu } from "@cloudflare/kumo";
import { CaretDownIcon } from "@phosphor-icons/react";

/**
 * Split button: a primary action joined with a dropdown trigger for related
 * secondary actions. The caret button uses `shape="square"` and an
 * `aria-label`.
 */
export function ButtonGroupSplitDemo() {
  return (
    <ButtonGroup aria-label="Deploy">
      <Button variant="primary">Deploy</Button>
      <DropdownMenu>
        <DropdownMenu.Trigger
          render={
            <Button
              variant="primary"
              shape="square"
              aria-label="More deploy options"
            >
              <CaretDownIcon />
            </Button>
          }
        />
        <DropdownMenu.Content>
          <DropdownMenu.Item>Deploy to staging</DropdownMenu.Item>
          <DropdownMenu.Item>Deploy and tail logs</DropdownMenu.Item>
          <DropdownMenu.Item>Schedule deploy…</DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu>
    </ButtonGroup>
  );
}

When to use

ButtonGroup joins a small set of tightly-coupled buttons into a single control — most commonly a split button: a primary action next to a dropdown trigger for related, secondary actions.

Grouping multiple independent buttons or inputs? Use Toolbar instead. A toolbar (e.g. a formatting bar or a page-level set of actions) needs roaming-focus keyboard semantics that ButtonGroup intentionally does not provide.

Installation

Barrel

import { ButtonGroup } from "@cloudflare/kumo";

Granular

import { ButtonGroup } from "@cloudflare/kumo/components/button-group";

Usage

ButtonGroup is a layout wrapper. Its children keep their own variant, size, and shape — the group only flattens the inner corners and overlaps borders so the buttons share a single seam. Give the group an aria-label describing the action, and give the dropdown trigger its own aria-label.

import { Button, ButtonGroup, DropdownMenu } from "@cloudflare/kumo";
import { CaretDownIcon } from "@phosphor-icons/react";

export default function Example() {
return (
  <ButtonGroup aria-label="Deploy">
    <Button variant="primary">Deploy</Button>
    <DropdownMenu>
      <DropdownMenu.Trigger
        render={
          <Button
            variant="primary"
            shape="square"
            aria-label="More deploy options"
          >
            <CaretDownIcon />
          </Button>
        }
      />
      <DropdownMenu.Content>
        <DropdownMenu.Item>Deploy to staging</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
  </ButtonGroup>
);
}

Examples

Split button

A primary action joined with a dropdown trigger for secondary actions.

import { Button, ButtonGroup, DropdownMenu } from "@cloudflare/kumo";
import { CaretDownIcon } from "@phosphor-icons/react";

/**
 * Split button: a primary action joined with a dropdown trigger for related
 * secondary actions. The caret button uses `shape="square"` and an
 * `aria-label`.
 */
export function ButtonGroupSplitDemo() {
  return (
    <ButtonGroup aria-label="Deploy">
      <Button variant="primary">Deploy</Button>
      <DropdownMenu>
        <DropdownMenu.Trigger
          render={
            <Button
              variant="primary"
              shape="square"
              aria-label="More deploy options"
            >
              <CaretDownIcon />
            </Button>
          }
        />
        <DropdownMenu.Content>
          <DropdownMenu.Item>Deploy to staging</DropdownMenu.Item>
          <DropdownMenu.Item>Deploy and tail logs</DropdownMenu.Item>
          <DropdownMenu.Item>Schedule deploy…</DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu>
    </ButtonGroup>
  );
}

Secondary

Split buttons work with any button variant.

import { Button, ButtonGroup, DropdownMenu } from "@cloudflare/kumo";
import { CaretDownIcon } from "@phosphor-icons/react";

/**
 * Split buttons work with any button variant — here the secondary style for a
 * lower-emphasis action.
 */
export function ButtonGroupSecondaryDemo() {
  return (
    <ButtonGroup aria-label="Save">
      <Button variant="secondary">Save</Button>
      <DropdownMenu>
        <DropdownMenu.Trigger
          render={
            <Button
              variant="secondary"
              shape="square"
              aria-label="More save options"
            >
              <CaretDownIcon />
            </Button>
          }
        />
        <DropdownMenu.Content>
          <DropdownMenu.Item>Save as draft</DropdownMenu.Item>
          <DropdownMenu.Item>Save and publish</DropdownMenu.Item>
          <DropdownMenu.Item>Save a copy…</DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu>
    </ButtonGroup>
  );
}

Sizes

Match the size on both buttons to keep the split button aligned.

import { Button, ButtonGroup, DropdownMenu } from "@cloudflare/kumo";
import { CaretDownIcon } from "@phosphor-icons/react";

/**
 * Match the `size` on both buttons to keep the split button aligned.
 */
export function ButtonGroupSizesDemo() {
  const sizes = ["sm", "base", "lg"] as const;
  return (
    <div className="flex flex-wrap items-center gap-4">
      {sizes.map((size) => (
        <ButtonGroup key={size} aria-label="Deploy">
          <Button size={size} variant="primary">
            Deploy
          </Button>
          <DropdownMenu>
            <DropdownMenu.Trigger
              render={
                <Button
                  size={size}
                  variant="primary"
                  shape="square"
                  aria-label="More deploy options"
                >
                  <CaretDownIcon />
                </Button>
              }
            />
            <DropdownMenu.Content>
              <DropdownMenu.Item>Deploy to staging</DropdownMenu.Item>
              <DropdownMenu.Item>Schedule deploy…</DropdownMenu.Item>
            </DropdownMenu.Content>
          </DropdownMenu>
        </ButtonGroup>
      ))}
    </div>
  );
}

API Reference

PropTypeDefaultDescription
childrenReactNode-The tightly-coupled controls to join. Typically two `Button`s: a primary action and a dropdown trigger (a "split button").
classNamestring-Additional CSS classes merged via `cn()`. Use kumo semantic tokens only.
idstring--
langstring--
titlestring--