feat(button-group): add ButtonGroup component - #640
Conversation
Joins related buttons into a single segmented control. Layout-only: children keep their own variant, size, and shape, while ButtonGroup flattens inner corners and overlaps borders so adjacent buttons share one seam. - Horizontal (default) and vertical orientation - Works for action rows, icon toolbars, and split buttons (primary action + dropdown trigger) - role="group" by default, overridable (e.g. role="toolbar") - Uses only kumo semantic tokens (no raw colors, no dark: variant) Includes unit tests, docs page + demos, HomeGrid showcase entry, sidebar nav entry, registry category mapping, and a changeset.
commit: |
Docs PreviewCommit: |
…t corners - Remove vertical orientation; ButtonGroup is horizontal-only - Fix seam jumping on hover: only lift z-index on keyboard focus, never on hover, so the shared 1px seam stays put (later sibling paints on top) - Make corner rounding robust to runtime-injected elements (dropdown popup / backdrop / focus guards) by scoping the join selectors to button/a via :*-of-type, so outer corners stay rounded when a menu opens
…ne control Each kumo Button ships its own shadow-xs. Inside a group those drop shadows overlap at the seams, making the middle button look elevated/boxed and leaving a clipped-corner artifact on its trailing edge. Drop child shadows within the group; the shared rings provide all the definition needed.
… dark box Root cause of the boxed/dark middle segment: kumo's Button draws a near-black ring on *any* focus (focus:ring-kumo-focus/50), including mouse clicks. Inside a segmented group that renders as a jarring dark box on the clicked segment. - Reset the ring to the resting line color for mouse focus (:focus:not(:focus-visible)); the fill/selected state is the feedback - Keep the keyboard focus-visible ring (lifted) for a11y Verified in a real browser across resting / hover / mouse-click / keyboard-focus for the segmented control, the primary split button (incl. open dropdown), and the icon toolbar.
mattrothenberg
left a comment
There was a problem hiding this comment.
Love this. We should probably use Base UI's Toolbar primitive here, no? https://base-ui.com/react/components/toolbar
ArshVermaGit
left a comment
There was a problem hiding this comment.
Looks good to me. The component is implemented cleanly, with clear docs, demos, and test coverage. It fits well with the existing component library. LGTM.
OK, my suggestion is not good here. Gonna offer a different idea instead
|
I took another look and I think I was being a little too broad with my original suggestion. I don't think every The part I am still worried about is encouraging this: <ButtonGroup role="toolbar" aria-label="Formatting">
{/* buttons */}
</ButtonGroup>Adding the role also opts us into the toolbar keyboard contract: one Tab stop, arrow-key navigation between controls, roving Could we keep // Related actions / split button
<ButtonGroup>
<Button>Deploy</Button>
<Button aria-label="More deployment options">...</Button>
</ButtonGroup>
// A real toolbar with toolbar keyboard behavior
<Toolbar aria-label="Text formatting">
<Toolbar.Button aria-label="Bold">...</Toolbar.Button>
<Toolbar.Button aria-label="Italic">...</Toolbar.Button>
<Toolbar.Button aria-label="Underline">...</Toolbar.Button>
</Toolbar>So I think the component itself is reasonable; I would just remove the |
Addresses review feedback about overlap with a toolbar. - Remove toolbar-flavored usage: ButtonGroup no longer sets/encourages role="toolbar"; it renders a plain role="group" (correct for a split button) - Drop the icon-toolbar, segmented, and action-row demos; ButtonGroup is now documented around the split-button pattern (primary action + dropdown) - Docs: add a 'When to use' section directing multi-button/input grouping to Toolbar (which is already built on Base UI's Toolbar primitive) - Update JSDoc, tests, and changeset accordingly Rationale: Base UI's Toolbar.Group requires a Toolbar.Root ancestor (roaming tabindex / arrow-key nav), which is wrong for a standalone split button. Real toolbars should use kumo Toolbar; ButtonGroup stays a minimal role=group join.
|
@mattrothenberg re: using Base UI's Toolbar primitive here — dug into this and reworked the component around it: kumo's
I deliberately did not build Net: real toolbars → |
|
@stritt 100% agree. Let's proceed with this approach! |








































































Summary
Adds a
ButtonGroupcomponent that visually joins related buttons into a single segmented control — action rows, icon toolbars, and split buttons (a primary action next to a dropdown trigger).Motivated by a hand-rolled split-button snippet that flattened corners manually and used raw Tailwind colors (
border-blue-700 dark:border-blue-600), which violate kumo's styling rules.ButtonGroupmakes this a first-class, reusable component built entirely on semantic tokens.Design
variant,size, andshape. The group flattens inner corners and overlaps borders (-1px) so adjacent buttons share a single seam — the seam color comes from each button's own ring, so it works for every variant without hardcoded colors.horizontal(default) andvertical.role="group"by default, overridable (e.g.role="toolbar"witharia-label). Interacted buttons lift viaz-indexso focus rings aren't clipped by the overlap.dark:— passes the custom color lint.What's included
KUMO_BUTTON_GROUP_VARIANTS,_DEFAULT_VARIANTS,_STYLING),forwardRef,displayName/components/button-group) with 6 demos (basic, split button, icon toolbar, icons+labels, vertical, sizes)Action) + changeset (minor)Testing
pnpm --filter @cloudflare/kumo test button-group→ 9/9 passpnpm --filter @cloudflare/kumo typecheck→ cleanpnpm --filter @cloudflare/kumo lint→ no button-group issuespnpm --filter @cloudflare/kumo build+ docs build (72 pages, button-group page renders)Reviews
bonk has reviewed the change
automated review not possible because: new component with visual/interaction behavior best verified by a human in the preview link
Tests
Tests included/updated