ComponentsButtonGroup
ButtonGroup
Keep related buttons together in a row or column.
On this page
Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.
View registry filesImport the component into your page or component file.
import {
Button,
ButtonGroup,
ButtonGroupSeparator,
} from "@dethink/components";Attached and separated groups share visual structure, never interaction state.
Action groups
Horizontal, vertical, mixed-state, icon-only, and separator compositions using the same two-prop layout contract.
Attached document actions
Separated state-aware actions
Vertical actions with a decorative separator
Show sourceexamples/button-group/basic.tsx
"use client";
import {
Button,
ButtonGroup,
ButtonGroupSeparator,
IconButton,
} from "@dethink/components";
import { MoreHorizontal } from "lucide-react";
export function ButtonGroupBasic() {
return (
<div className="space-y-8">
<div className="space-y-3">
<p className="text-muted-foreground text-sm font-medium">
Attached document actions
</p>
<ButtonGroup aria-label="Document actions">
<Button variant="outline">Preview</Button>
<Button variant="outline">Share</Button>
<IconButton aria-label="More document actions" variant="outline">
<MoreHorizontal />
</IconButton>
</ButtonGroup>
</div>
<div className="space-y-3">
<p className="text-muted-foreground text-sm font-medium">
Separated state-aware actions
</p>
<ButtonGroup aria-label="Publishing actions" mode="separated">
<Button>Publish</Button>
<Button disabled variant="outline">
Schedule
</Button>
<Button loading variant="outline">
Syncing
</Button>
</ButtonGroup>
</div>
<div className="space-y-3">
<p className="text-muted-foreground text-sm font-medium">
Vertical actions with a decorative separator
</p>
<ButtonGroup aria-label="Record actions" orientation="vertical">
<Button variant="outline">Duplicate</Button>
<ButtonGroupSeparator />
<Button variant="outline">Archive</Button>
<Button variant="destructive">Delete</Button>
</ButtonGroup>
</div>
</div>
);
}The product declares one container threshold and keeps action ownership stable across representations.
Wide group to narrow overflow
The narrow representation is the no-container-query fallback. At the declared container threshold, the same action definitions render in ButtonGroup; IDs, labels, disabled rules, destructive meaning, and handlers are preserved.
Quarterly report
Quarterly report
Choose an available action.
Show sourceexamples/button-group/responsive-action-handoff.tsx
"use client";
import { useState } from "react";
import {
Button,
ButtonGroup,
DropdownButton,
DropdownMenuItem,
} from "@dethink/components";
type HeaderActionId = "preview" | "share" | "export" | "archive";
interface HeaderAction {
destructive?: boolean;
disabled?: boolean;
id: HeaderActionId;
label: string;
}
const headerActions: HeaderAction[] = [
{ id: "preview", label: "Preview" },
{ id: "share", label: "Share" },
{ disabled: true, id: "export", label: "Export pending approval" },
{ destructive: true, id: "archive", label: "Archive" },
];
const alwaysVisibleIds = new Set<HeaderActionId>(["preview", "share"]);
export function ButtonGroupResponsiveActionHandoff() {
const [lastActionId, setLastActionId] = useState<HeaderActionId>();
const wideActions = headerActions;
const narrowPrimaryActions = headerActions.filter((action) =>
alwaysVisibleIds.has(action.id),
);
const narrowMenuActions = headerActions.filter(
(action) => !alwaysVisibleIds.has(action.id),
);
const runAction = (action: HeaderAction) => setLastActionId(action.id);
return (
<div className="@container space-y-4">
<div
data-layout="wide"
className="hidden @min-3xl:flex @min-3xl:items-center @min-3xl:justify-between @min-3xl:gap-4"
>
<p className="text-sm font-medium">Quarterly report</p>
<ButtonGroup aria-label="Quarterly report actions">
{wideActions.map((action) => (
<Button
key={action.id}
data-action-id={action.id}
disabled={action.disabled}
onClick={() => runAction(action)}
variant={action.destructive ? "destructive" : "outline"}
>
{action.label}
</Button>
))}
</ButtonGroup>
</div>
<div
data-layout="narrow"
className="flex items-center justify-between gap-3 @min-3xl:hidden"
>
<p className="min-w-0 truncate text-sm font-medium">Quarterly report</p>
<ButtonGroup aria-label="Quarterly report actions" mode="separated">
{narrowPrimaryActions.map((action) => (
<Button
key={action.id}
data-action-id={action.id}
disabled={action.disabled}
onClick={() => runAction(action)}
variant="outline"
>
{action.label}
</Button>
))}
<DropdownButton
aria-label="More quarterly report actions"
label="More"
size="sm"
>
{narrowMenuActions.map((action) => (
<DropdownMenuItem
key={action.id}
data-action-id={action.id}
destructive={action.destructive}
disabled={action.disabled}
onAction={() => runAction(action)}
>
{action.label}
</DropdownMenuItem>
))}
</DropdownButton>
</ButtonGroup>
</div>
<p aria-live="polite" className="text-muted-foreground text-sm">
{lastActionId
? `Application handled action ID: ${lastActionId}`
: "Choose an available action."}
</p>
</div>
);
}This is an application recipe, not automatic component behavior. ButtonGroup never measures children, infers priority, hides actions, installs a ResizeObserver, or becomes a Toolbar. Choose the threshold and the always-visible action IDs from product requirements.
ButtonGroup is deliberately narrower than the components it is often confused with.
- ButtonGroup
- Use for a small set of related independent actions. Every enabled control stays in the normal Tab order; arrow keys do nothing special.
- ToggleGroup
- Use when controls represent single or multiple pressed selections and the selected value belongs to the group.
- Toolbar
- Use for a command surface that requires toolbar semantics, roving focus, and arrow-key navigation.
- DropdownButton
- Use when a visible action opens a menu, or when a primary action and its alternatives form a split button.
- InputGroup
- Use when controls are attached to an input and participate in a field's labelling, validation, or editing workflow.
Label the relationship; leave each action's behavior native.
Supply aria-label or aria-labelledby on every group. Icon-only children still need their own accessible names.
Tab and Shift+Tab visit each enabled child in document order. Enter and Space activate the focused native button. ButtonGroup does not add arrow-key navigation, selection values, pressed state, shared loading, child measurement, or automatic overflow.
| Prop | What it does | Default |
|---|---|---|
mode"attached" | "separated" | Merge adjacent borders and logical corners, or retain a density-token gap between actions. | "attached" |
orientation"horizontal" | "vertical" | Set the visual flow. Each child remains in normal document Tab order in either orientation. | "horizontal" |
aria-label / aria-labelledbystring | Give the role=group container a concise accessible name that describes the relationship between its actions. | Not set |
childrenReactNode | Independent Button, IconButton, native button, or compatible action elements. ButtonGroup does not clone them. | Not set |
classNamestring | Compose placement and sizing utilities. Keep the internal axis aligned with orientation; responsive action handoff belongs to the surrounding product layout. | Not set |
| Prop | What it does | Default |
|---|---|---|
classNamestring | Compose classes onto the decorative token-backed rule. Its axis is derived from the surrounding group. | Not set |