Button group
Related actions joined into one surface with hairline dividers: a hover highlight glides between segments, the pressed one answers in place, and an attached menu can close the row.
actiongrouptoolbarnew
Preview
Button group preview
Live · keyboard ready
When to use
- A small set of related actions on the same object, shown together in a header, toolbar or card footer.
- Compact tool clusters such as zoom out, the current level and zoom in, or stacked map controls.
- A primary action row, such as Reply, Reply all and Forward on a message.
When not to use
- Use toggle-group or segmented-control when the segments are options that stay selected.
- Use split-button when there is one main action and a few variants of it.
- Use separate buttons when the actions are unrelated or one of them is much more important than the rest.
- Use tabs for switching between views.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "button-group",
"type": "registry:ui",
"title": "Button group",
"description": "Related actions joined into one surface with hairline dividers: a hover highlight glides between segments, the pressed one answers in place, and an attached menu can close the row.",
"categories": [
"actions"
],
"dependencies": [
"@radix-ui/react-dropdown-menu",
"lucide-react",
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/button-group/button-group.tsx",
"type": "registry:component",
"target": "@components/manicat/button-group/button-group.tsx"
},
{
"path": "registry/components/button-group/button-group.module.css",
"type": "registry:component",
"target": "@components/manicat/button-group/button-group.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/button-group",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/button-group",
"markdown": "https://uiarc.dev/components/button-group/markdown",
"tags": [
"action",
"group",
"toolbar",
"new"
]
}
}Usage
Install the component, then copy this into your app. Change the content and the props to fit your screen.
example.tsxtsx
"use client";
import { MoreHorizontal, Share2, Star, Trash2, Undo2 } from "lucide-react";
import { ButtonGroup } from "@/components/manicat/button-group/button-group";
export function ButtonGroupExample() {
return (
<ButtonGroup
label="Document actions"
items={[
{ id: "copy", label: "Copy link", icon: <Share2 size={16} />, onSelect: () => {} },
{ id: "star", label: "Star", icon: <Star size={16} />, onSelect: () => {} },
{ id: "delete", label: "Delete", icon: <Trash2 size={16} />, onSelect: () => {} },
]}
menu={{ label: "More actions", items: [{ id: "move", label: "Move to folder" }, { id: "archive", label: "Archive" }] }}
/>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Document toolbar Primary actions inline and the rest behind a “more” menu.
Document toolbar
Primary actions inline and the rest behind a “more” menu. preview
Live · keyboard ready
document-toolbar-primary-actions-inline-and-the-rest-behind-a-more-menu-.tsxtsx
Keyboard interactions
| Keys | Action |
|---|---|
| Tab / Shift+Tab | Moves into, through and out of the group. Every segment is a tab stop, so the group behaves like the buttons it replaces. |
| Arrow right / Arrow left | Also moves between segments in a horizontal group, wrapping at the ends (mirrored in right to left layouts). |
| Arrow down / Arrow up | Moves between segments in a vertical group. On the chevron segment, Arrow down opens the menu instead. |
| Home / End | Moves to the first or last segment. |
| Enter / Space | Activates the focused segment. Link segments follow the link with Enter. |
| Enter / Space / Arrow down | On the chevron segment: opens the menu with the first item highlighted. |
| Arrow keys / Home / End | In the menu: moves between items, looping at the ends; disabled items are skipped. |
| Escape | Closes the menu and returns focus to the chevron. |
| Tab (menu open) | Closes the menu and moves on, since the menu is non-modal. |
Accessibility
- The root is a div with role="group" named by label. Segments are native buttons (or links with href), so they keep their own roles and activation keys.
- Disabled segments use aria-disabled instead of the disabled attribute, so they stay focusable and are read as dimmed; their onSelect never runs. A disabled whole group uses the disabled attribute.
- Icon-only segments, segments with content and collapsed segments take their label as aria-label and as a tooltip; icons are hidden from assistive technology.
- When a segment's label changes within a second of its press, such as "Share" to "Copied", a polite live region in the group announces the new label once; the quiet return to "Share" is not announced.
- The chevron segment is a Radix dropdown trigger named by menu.label, with aria-expanded and aria-haspopup. The menu manages focus, typeahead, Escape and outside clicks, and returns focus to the chevron. It is non-modal, so opening it never locks scroll or shifts the page.
- Keyboard focus moves the same highlight the pointer does; pointer focus draws no ring and keyboard focus shows Arc's shared focus ring. A mouse click focuses without the highlight, so nothing stays lit after the pointer leaves.
- State is never carried by color alone: confirmations change the label and icon, and disabled segments are dimmed and announced.
Motion
- One highlight lives under all segments. Moving the pointer or keyboard focus to another segment carries it there on the snappy spring; arriving from outside the group it fades in where it lands, so it only travels between segments.
- The hairline dividers on either side of the highlighted segment fade out, so the highlight reads as one soft shape inside the joined surface.
- A press dips the segment's content to 0.97 (0.9 for icon-only) and springs back, while the highlight under it deepens. The group and its neighbours never move. The chevron anchors the menu, so it answers with the deeper highlight only.
- On touch, the highlight appears under the finger while pressed and fades on release.
- Nothing in the group moves or resizes on hover, focus, press, menu open or a label change: every reserved label sits invisibly in the segment, so a confirmation crossfades inside a fixed box. The new label rises in from a soft blur while the old one lifts away.
- A label that was not reserved never snaps either: the segment springs to its width on the morph spring, and the highlight stays locked to it while it resizes.
- The highlight is measured at sub-pixel precision, so it sits exactly on its segment at rest.
- The menu grows from the group's edge with a short offset and scale and leaves faster than it arrives; the chevron turns over while it is open.
- With reduced motion, the highlight jumps between segments, the press dip and chevron turn are removed, and labels and the menu crossfade.
Responsive behavior
- A horizontal group never exceeds its container: when it would, segments with an icon drop their label (collapseLabels), and anything still too wide scrolls sideways without a visible scrollbar. At 320px the document example collapses to icons with its names kept as tooltips and aria-labels.
- The vertical orientation suits narrow rails and canvas corners; segments stretch to the widest one.
- Segments are 34px tall at sm and 42px at md inside the border, matching the button heights. The highlight follows the pointer only for mouse and pen; touch gets it while pressed.
Performance
- The highlight is one element moved by transforms and size motion values; segments never re-render on hover beyond a state update in the group.
- Two ResizeObservers per group keep the highlight on its segment and decide when labels collapse; each label slot has its own observer for the width morph.
- The menu renders only while open.
Notes for AI
- Choose it for two to five related actions on one object, such as a document's Share, Duplicate and Archive, or zoom controls. Put rarely used and destructive actions in menu.
- It is an action group: every segment does something now. For choosing one or more options that stay pressed, use toggle-group or segmented-control instead.
- Give confirmations by changing an item's label and icon ("Share" to "Copied") and resetting it after a moment, and list every label in reserve so the row never resizes. Keep confirmation labels close in length to the resting label, since the segment takes the widest.
- Use iconOnly only for actions with universally understood icons (zoom, alignment, playback). The label still names the action for screen readers and the tooltip.
- Use the solid variant for the main actions of a view, at most once per screen; outline for everything else.