Button Group
Related buttons joined into one segmented control. For choosing between mutually exclusive options use ToggleGroup instead — a button group is a set of actions, not a value.
Added 14 October 2026
Button Group or Toggle Group?
- Button Group — several actions that belong together: a split button, an Export + chevron pair, a stepper’s minus/plus around a value. Nothing stays pressed.
- Toggle Group — a value with 2–7 options where one (or several) stays selected. If a button should look active after clicking, you want ToggleGroup.
The parts
<ButtonGroup aria-label="Export">
<Button variant="outline">Export</Button>
<ButtonGroupSeparator />
<Button variant="outline" size="icon" aria-label="More export options">
<ChevronDownIcon />
</Button>
</ButtonGroup>
<div hlmButtonGroup aria-label="Export">
<button hlmBtn variant="outline">Export</button>
<hlm-button-group-separator />
<button hlmBtn variant="outline" size="icon" aria-label="More export options">
<ng-icon name="lucideChevronDown" />
</button>
</div>
ButtonGroupText/hlmButtonGroupText— a non-interactive label welded into the group (a unit, a count, a currency). Use it for the value in a minus/value/plus stepper.ButtonGroupSeparator/hlm-button-group-separator— an explicit divider, mainly for split buttons where the two halves do different things. It needs no props: it reads the group’s axis and orients itself across it, in both frameworks and both orientations.orientation="vertical"stacks the group. Radius, borders and the separator all follow.
Button’s own sizes cooperate with the group automatically (size="sm", "icon-sm" and the
xs sizes carry in-data-[slot=button-group] hooks), so mixed sizes inside one group stay
square-cornered where they meet. Don’t hand-tune radii per child.
Floating (over a map)
A group whose buttons are all variant="floating" becomes one floating control: the group
carries the radius and the shadow, the buttons drop theirs, and a border-border hairline
separates them. Set the variant on the buttons only — the group picks it up by itself.
<ButtonGroup orientation="vertical" aria-label="Map zoom">
<Button variant="floating" size="icon" aria-label="Zoom in"><PlusIcon /></Button>
<Button variant="floating" size="icon" aria-label="Zoom out"><MinusIcon /></Button>
</ButtonGroup>
- All members floating, or none. A mixed group gets a shadow around everything and a double edge on the non-floating members.
- No
ButtonGroupSeparatorinside — the hairline is already there; a separator doubles it. - Members must be direct children. A floating button wrapped in a tooltip/popover host element misses the group styling.
- The filled-variant seam below does not apply: a floating button paints under its border.
Accessibility
- Name the group.
role="group"with no accessible name announces as bare “group” followed by unrelated-sounding buttons.aria-label="Date granularity"turns three buttons into one control. [a11y] - Name every icon-only child. lucide and
@ng-iconsboth mark their glyphsaria-hidden, so an icon button with noaria-labelhas no accessible name. This bites hardest here, because chevron/minus/plus buttons are the most common group members. [a11y] - The focus ring is lifted above neighbours (
*:focus-visible:relative *:focus-visible:z-10) and nothing clips it — don’t addoverflow-hiddento the group.
Known issue: filled variants show a hairline seam
Between two adjacent filled buttons (variant="default", "secondary", "destructive")
a 1px line of the page shows through. The group strips only the leading border, so the
leading sibling keeps its own transparent trailing border, and Button sets bg-clip-padding
so its background does not paint under it. outline groups are unaffected — there the
retained border is the divider you want.
Until it is fixed (both candidate fixes reach into Button, so it needs a decision there),
prefer variant="outline" for grouped buttons, which is the shadcn default for this pattern
anyway. Visible in the showcase’s “Filled variant” row.
Rules
- Keep members the same
variantandsize. A group is one control; mixing emphasis inside it reads as a layout accident. - Two to five members. Beyond that use a
Selector aDropdownMenu. - A split button’s primary half carries the label, the chevron half only opens the menu —
give the chevron its own
aria-label, not a copy of the primary’s. - Nested groups get
gap-2automatically (has-[>[data-slot=button-group]]:gap-2); use nesting to separate groups of actions, not to fake spacing.
Never
- A button group as a tab bar — use
Tabs. - A button group where one member is a link and the rest are buttons; the shared shape promises shared behaviour.
role="radiogroup"oraria-pressedon members. If state persists, it is a ToggleGroup.