Skip to content
Navigation

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 ButtonGroupSeparator inside — 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-icons both mark their glyphs aria-hidden, so an icon button with no aria-label has 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 add overflow-hidden to 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 variant and size. A group is one control; mixing emphasis inside it reads as a layout accident.
  • Two to five members. Beyond that use a Select or a DropdownMenu.
  • 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-2 automatically (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" or aria-pressed on members. If state persists, it is a ToggleGroup.