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
| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | - | The tightly-coupled controls to join. Typically two `Button`s: a primary action and a dropdown trigger (a "split button"). |
| className | string | - | Additional CSS classes merged via `cn()`. Use kumo semantic tokens only. |
| id | string | - | - |
| lang | string | - | - |
| title | string | - | - |