Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/button-group.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/kumo": minor
---

Add `ButtonGroup` component for joining a small set of tightly-coupled buttons into a single control — most commonly a split button (a primary action next to a dropdown trigger). Handles layout only: children keep their own variant, size, and shape while the group flattens inner corners and overlaps borders so the buttons share one seam. Renders `role="group"`. For grouping multiple independent buttons or inputs, use `Toolbar` instead.
1 change: 1 addition & 0 deletions packages/kumo-docs-astro/src/components/SidebarNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const componentItems: NavItem[] = [
{ label: "Banner", href: "/components/banner" },
{ label: "Breadcrumbs", href: "/components/breadcrumbs" },
{ label: "Button", href: "/components/button" },
{ label: "Button Group", href: "/components/button-group" },
{ label: "Checkbox", href: "/components/checkbox" },
{ label: "Clipboard Text", href: "/components/clipboard-text" },
{ label: "Cloudflare Logo", href: "/components/cloudflare-logo" },
Expand Down
99 changes: 99 additions & 0 deletions packages/kumo-docs-astro/src/components/demos/ButtonGroupDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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>
);
}

/**
* 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>
);
}

/**
* 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>
);
}
15 changes: 15 additions & 0 deletions packages/kumo-docs-astro/src/components/demos/HomeGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Badge,
Banner,
Button,
ButtonGroup,
Checkbox,
ClipboardText,
Collapsible,
Expand Down Expand Up @@ -43,6 +44,7 @@ import { ShikiProvider, CodeHighlighted } from "@cloudflare/kumo/code";
import { CommandPaletteBasicDemo } from "~/components/demos/CommandPaletteDemo";
import { InputGroupDemo } from "~/components/demos/InputGroupDemo";
import {
CaretDownIcon,
MagnifyingGlassIcon,
PlusIcon,
TranslateIcon,
Expand All @@ -56,6 +58,7 @@ const componentRoutes: Record<string, string> = {
banner: "/components/banner",
breadcrumbs: "/components/breadcrumbs",
button: "/components/button",
"button-group": "/components/button-group",
checkbox: "/components/checkbox",
"clipboard-text": "/components/clipboard-text",
"code-highlighted": "/components/code-highlighted",
Expand Down Expand Up @@ -138,6 +141,18 @@ export function HomeGrid() {
</div>
),
},
{
name: "Button Group",
id: "button-group",
Component: (
<ButtonGroup>
<Button variant="primary">Deploy</Button>
<Button variant="primary" shape="square" aria-label="More options">
<CaretDownIcon />
</Button>
</ButtonGroup>
),
},
{
name: "Input",
id: "input",
Expand Down
152 changes: 152 additions & 0 deletions packages/kumo-docs-astro/src/pages/components/button-group.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
layout: ~/layouts/MdxDocLayout.astro
title: "Button Group"
description: "Joins a primary action and a dropdown trigger into a single split button."
sourceFile: "components/button-group"
---

import ComponentExample from "~/components/docs/ComponentExample.astro";
import ComponentSection from "~/components/docs/ComponentSection.astro";
import CodeBlock from "~/components/docs/CodeBlock.astro";
import PropsTable from "~/components/docs/PropsTable.astro";
import {
ButtonGroupSplitDemo,
ButtonGroupSecondaryDemo,
ButtonGroupSizesDemo,
} from "~/components/demos/ButtonGroupDemo";

{/* Demo */}

<ComponentSection>
<ComponentExample
demo="ButtonGroupSplitDemo"
vrSection="basic"
vrTitle="Basic"
>
<ButtonGroupSplitDemo client:visible />
</ComponentExample>
</ComponentSection>

{/* When to use */}

<ComponentSection>

## When to use

<p>
`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.
</p>

<p>
<strong>Grouping multiple independent buttons or inputs?</strong> Use{" "}
<a href="/components/toolbar">Toolbar</a> 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.
</p>

</ComponentSection>

{/* Installation */}

<ComponentSection>

## Installation

### Barrel

<CodeBlock
code={`import { ButtonGroup } from "@cloudflare/kumo";`}
lang="tsx"
/>

### Granular

<CodeBlock
code={`import { ButtonGroup } from "@cloudflare/kumo/components/button-group";`}
lang="tsx"
/>
</ComponentSection>

{/* Usage */}

<ComponentSection>

## Usage

<p>
`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`.
</p>

<CodeBlock
code={`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>
);
}`}
lang="tsx"
/>
</ComponentSection>

{/* Examples */}

<ComponentSection>

## Examples

### Split button

<p>A primary action joined with a dropdown trigger for secondary actions.</p>
<ComponentExample demo="ButtonGroupSplitDemo">
<ButtonGroupSplitDemo client:visible />
</ComponentExample>

### Secondary

<p>Split buttons work with any button variant.</p>
<ComponentExample demo="ButtonGroupSecondaryDemo">
<ButtonGroupSecondaryDemo client:visible />
</ComponentExample>

### Sizes

<p>Match the `size` on both buttons to keep the split button aligned.</p>
<ComponentExample demo="ButtonGroupSizesDemo">
<ButtonGroupSizesDemo client:visible />
</ComponentExample>

</ComponentSection>

{/* API Reference */}

<ComponentSection>

## API Reference

<PropsTable component="ButtonGroup" />
</ComponentSection>
4 changes: 4 additions & 0 deletions packages/kumo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@
"types": "./dist/components/button.d.ts",
"import": "./dist/components/button.js"
},
"./components/button-group": {
"types": "./dist/components/button-group.d.ts",
"import": "./dist/components/button-group.js"
},
"./components/chart": {
"types": "./dist/components/chart.d.ts",
"import": "./dist/components/chart.js"
Expand Down
1 change: 1 addition & 0 deletions packages/kumo/scripts/component-registry/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
export const CATEGORY_MAP: Record<string, string> = {
// Action
button: "Action",
"button-group": "Action",
"clipboard-text": "Action",
// Display
badge: "Display",
Expand Down
Loading
Loading