Expanding button group
Icon buttons in a compact group: the one you point at or focus grows to reveal its label while its neighbours slide aside, and an action confirms in place.
actiongrouptoolbarnew
Preview
Expanding button group preview
Live · keyboard ready
When to use
- A compact action bar on a message, file, or card where space is tight but labels should be one glance away.
- Toolbars where actions finish in place and deserve a confirmation without a toast, such as Archive or Copy link.
When not to use
- Use toggle-group or segmented-control when the buttons select a value instead of running an action.
- Use plain labelled buttons when there are only one or two actions and space allows; hiding labels then costs clarity.
- Use a dropdown menu for more than about six actions, or for actions that need descriptions.
shadcn CLI
Also installs (1)
- manicat-foundation
Manual install(+)
registry.jsonjson
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "expanding-button-group",
"type": "registry:ui",
"title": "Expanding button group",
"description": "Icon buttons in a compact group: the one you point at or focus grows to reveal its label while its neighbours slide aside, and an action confirms in place.",
"categories": [
"actions"
],
"dependencies": [
"motion"
],
"registryDependencies": [
"https://manicat.viraza.net/r/manicat-foundation.json"
],
"files": [
{
"path": "registry/components/expanding-button-group/expanding-button-group.tsx",
"type": "registry:component",
"target": "@components/manicat/expanding-button-group/expanding-button-group.tsx"
},
{
"path": "registry/components/expanding-button-group/expanding-button-group.module.css",
"type": "registry:component",
"target": "@components/manicat/expanding-button-group/expanding-button-group.module.css"
}
],
"docs": "Docs and live preview: https://uiarc.dev/components/expanding-button-group",
"meta": {
"tier": "free",
"kind": "component",
"docs": "https://manicat.viraza.net/components/expanding-button-group",
"markdown": "https://uiarc.dev/components/expanding-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 { ExpandingButtonGroup } from "@/components/manicat/expanding-button-group/expanding-button-group";
export function ExpandingButtonGroupExample() {
return (
<ExpandingButtonGroup
label="Message actions"
items={[
{ id: "archive", label: "Archive", icon: <Star size={16} />, doneLabel: "Archived" },
{ id: "delete", label: "Delete", icon: <Trash2 size={16} />, tone: "danger" },
]}
/>
);
}
Examples
Real screens rather than isolated props. Switch to Code to copy a whole example into your project.
Message actions Icon buttons that expand to reveal their label; the destructive one uses the danger tone.
Message actions
Icon buttons that expand to reveal their label; the destructive one uses the danger tone. preview
Live · keyboard ready
message-actions-icon-buttons-that-expand-to-reveal-their-label-the-destructive-one-uses-the-danger-tone-.tsxtsx
Keyboard interactions
| Keys | Action |
|---|---|
| Tab | Moves into the toolbar onto one action (the last focused, else the resting one) and out again. |
| Arrow left / Arrow right | Moves focus to the previous or next action, wrapping at the ends. The focused action expands to show its label. |
| Home / End | Moves focus to the first or last action. |
| Enter / Space | Runs the focused action. |
Accessibility
- The root is a toolbar named by label, with one tab stop and roving arrow key focus.
- Each button keeps its full label as its accessible name while collapsed; the visible label and icon are hidden from assistive technology so nothing is read twice.
- Disabled actions use aria-disabled instead of disabled, so they stay focusable and still reveal their label, but do nothing when activated.
- A confirmation is announced through a polite live region, such as "Archived". The same word is announced again on a repeat action.
- A busy action (a promise from onSelect) sets aria-busy until it settles.
- The expansion itself is the focus indicator; pointer focus draws no ring and keyboard focus shows Arc's shared focus ring. Only keyboard focus expands, so a mouse click does not leave a label open after the pointer leaves.
- Confirmation is never color alone: the icon becomes a check and the label changes to a past tense word.
Motion
- Each button's width springs on the morph spring between its icon size and icon plus measured label. Neighbours slide aside in normal flow and the group's own width follows, so nothing resizes in a single frame.
- The label slot always reserves the longer of label and doneLabel, so the confirmation never changes the width. The server render already shows the resting action at that exact width, so hydration moves nothing.
- Anchor the group at its start edge (not centred) and reserve its widest width in the layout, so expansion only moves buttons inside the group.
- The icon keeps the same left inset in both states; the label slides out from behind it with a short fade and blur, and tucks back faster than it came.
- A confirmation draws a check where the icon was and lifts the old word out while the new one rises in, while the width springs between the two words.
- Hover only follows real pointer movement, and moving to a neighbour needs the pointer 4px inside it, so a button that slides under a resting pointer never takes over and a pointer resting on a boundary never flips between two actions.
- Pressing shrinks the icon slightly; the button box never scales, because its width is the animation.
- With reduced motion, labels and widths change at once with a quick fade and the check appears without drawing.
Responsive behavior
- Five medium actions with one label open take about 260 to 280px, depending on the longest label. Reserve that width in narrow layouts rather than letting the group squeeze content.
- Touch has no hover: the first tap expands an action and the second runs it. A tap outside the group closes a label opened by touch.
- Buttons are 36px tall at md and 28px at sm; use md where touch matters.
Performance
- Each label is measured from invisible copies in its own slot (and again if its font or text changes) with a ResizeObserver; width is a Motion value, so React renders only when the expanded action changes.
- Width animation causes layout in the group only; keep the group out of large reflowing containers.
Notes for AI
- Use it for a row of three to six related actions on one object, where icons are recognisable but labels help: message, file and card actions.
- Give every action a short verb label and a matching lucide icon. Use doneLabel only for actions that finish at once (Archive, Snooze, Copy link); leave it out for actions that open something (Reply, Rename).
- Mark at most one destructive action with tone="danger". It needs no confirm dialog when the surrounding UI offers Undo.
- One action is always expanded. Put the primary action first, or set defaultExpanded to it, so it reads as a labelled button at rest.
- This is an action group. For choosing one or several values use toggle-group or segmented-control.
- On touch the first tap reveals the label and a second tap runs it; the resting action runs on the first tap.
- Place it start aligned in a row that reserves its widest width, so the expansion never moves surrounding content.