v0.5

Button Group

Joined row of buttons that share a single bordered surface for segmented toolbars and view switchers.

Description

ButtonGroup is a compound component that paints a single joined surface around a row of ButtonGroupItems. The root renders as a <div role="group">; each item renders as a <button> (or any element via asChild), and the group's shared border, outer radius, and shadow give the cluster a unified look while individual hover, active, and focus states stay per-item.

Reach for it when several actions belong together visually: segmented view switchers (list / grid / gallery), inline action clusters in tables and headers, paging controls, formatting toolbars, or any place a single bordered surface communicates "these options are siblings." The root broadcasts variant and size through context so every item picks up the same sizing without repeating props on the children.

Don't use ButtonGroup for a single standalone action; reach for Button directly. If the user is picking one option out of several and the choice should persist visibly, prefer ToggleGroup (or Tabs when the labels also switch sibling views). For an overflow menu of unrelated actions that don't all need to be visible, use DropdownMenu.

Installation

pnpm dlx @create-ui/cli add button-group

Anatomy

<ButtonGroup>
  <ButtonGroupItem />
  <ButtonGroupItem />
  <ButtonGroupItem />
</ButtonGroup>

Usage

import { ButtonGroup, ButtonGroupItem } from "@/components/ui/button-group"
<ButtonGroup>
  <ButtonGroupItem>List</ButtonGroupItem>
  <ButtonGroupItem active>Grid</ButtonGroupItem>
  <ButtonGroupItem>Gallery</ButtonGroupItem>
</ButtonGroup>

Examples

Variants

variant sets the visual tone of the joined surface and cascades to every item via context. primary highlights the active item with the brand color, neutral is the high-contrast option for dark active surfaces, and soft keeps the cluster quiet for dense toolbars.

Shape

shape controls the outer radius of the group. rounded (default) follows the size's radius scale, pill is fully rounded for compact switchers, and square leaves the corners flush, which is useful when the group sits against another bordered surface.

Sizes

size accepts xs, sm, md, lg, and xl. The size is set on the root and inherited by every item; setting size on an individual ButtonGroupItem is supported but rarely needed.

With Icon

Each item accepts leadingIcon and trailingIcon. The wrappers carry data-slot="button-group-item-icon" and size themselves to match the active size, so icons stay aligned with the label across the full size scale.

Icon Only

iconOnly collapses an item to a square footprint with no horizontal padding, the standard pattern for formatting toolbars. Always pair it with an aria-label so screen readers announce the action.

States

Items expose three state props: active (writes data-active="true" and aria-pressed), loading (swaps the trailing slot for a spinner and sets aria-busy), and disabled (sets aria-disabled plus the native disabled attribute). Use active for the currently-selected segment in a view switcher.

Composition

ButtonGroupItem accepts asChild to render any element (commonly an <a>) while keeping the joined surface and state styling. Items also forward children into a flex slot, so you can nest a Badge for counts inline.

Archive

Accessibility

ButtonGroup writes role="group" on the root so assistive tech announces the cluster as a single related group of controls. Each ButtonGroupItem is a real <button> (or whatever element you pass via asChild), so focus, activation, and keyboard semantics come from the underlying element.

KeyDescription
TabMoves focus to the next item in the group.
Shift + TabMoves focus to the previous item in the group.
SpaceActivates the focused item (native button behavior).
EnterActivates the focused item (native button behavior).

ARIA notes:

  • The root sets role="group"; pass an aria-label or aria-labelledby on the ButtonGroup when the cluster needs a name beyond its visible content.
  • active writes aria-pressed="true" on the item so the selected segment is announced as a toggle state. The attribute is omitted entirely when active is false.
  • loading writes aria-busy="true" and suppresses pointer events while the spinner is visible.
  • disabled mirrors to aria-disabled and the native disabled attribute, so the item is removed from tab order.
  • When using iconOnly, always provide aria-label on the item; the icon alone is not announced.

Styling

Tailwind override: pass className to merge Tailwind classes with the component's CVA classes (via cn()):

<ButtonGroup className="shadow-none">
  <ButtonGroupItem className="font-mono">List</ButtonGroupItem>
</ButtonGroup>

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="button-group" on the root, with data-variant="<variant>", data-shape="<shape>", data-size="<size>", and role="group".
  • data-slot="button-group-item" on each item, with data-variant, data-size, and data-active="true" (only present when active).
  • data-slot="button-group-item-icon" on each leading / trailing icon wrapper.
  • data-slot="button-group-item-content" on the inner text/children wrapper.
  • data-slot="button-group-item-spinner" on the loading spinner SVG.

Target a specific state in CSS:

[data-slot="button-group-item"][data-active="true"] {
  /* … */
}
  • Button: use this for a single, standalone action that doesn't need to sit inside a joined surface.
  • ToggleGroup: mutually-exclusive selection where one option stays visibly highlighted.
  • Tabs: when the labels act as primary navigation between sibling views.
  • DropdownMenu: overflow menu for unrelated actions that don't all fit inline.

API Reference

ButtonGroup

Compound root that paints the joined surface and broadcasts variant and size to every ButtonGroupItem via context. Sets role="group" and writes data-slot="button-group" plus data-variant, data-shape, and data-size on the root. Extends React.ComponentProps<"div">, so any standard div attribute (id, aria-label, onClick, etc.) is accepted.

Props

PropTypeDefaultDescription
classNamestring-Tailwind classes merged with the component's CVA classes via cn().
childrenReact.ReactNode-One or more ButtonGroupItem elements.

Variants

VariantOptionsDefaultDescription
variant"primary" "neutral" "soft""primary"Visual tone of the cluster; cascades to every item via context.
shape"rounded" "pill" "square""rounded"Outer radius. pill is fully rounded; square leaves the corners flush.
size"xs" "sm" "md" "lg" "xl""md"Size scale; controls item height, padding, type, and icon size.

ButtonGroupItem

A single segment. Renders as a <button> by default, or as any element when asChild is true (via Radix's Slot). Inherits variant and size from the surrounding ButtonGroup unless overridden. Writes data-slot="button-group-item" plus data-variant, data-size, and data-active, and forwards aria-pressed / aria-busy / aria-disabled from active / loading / disabled. Extends React.ComponentProps<"button">, so any standard button attribute is accepted.

Props

PropTypeDefaultDescription
asChildbooleanfalseRender as the child element via Radix Slot. Use to make the item an <a> or other native element.
loadingbooleanfalseSwaps the trailing slot for a spinner and sets aria-busy. Pointer events are suppressed.
activebooleanfalseMarks the item as the selected segment. Writes data-active="true" and aria-pressed.
leadingIconReact.ReactNode-Icon rendered before the label. When iconOnly and loading are both true, replaced by the spinner.
trailingIconReact.ReactNode-Icon rendered after the label. Replaced by the spinner while loading.
iconOnlybooleanfalseSquare footprint with no horizontal padding; requires aria-label.
disabledbooleanfalseDisables the item; mirrored to aria-disabled and the native disabled attribute.
classNamestring-Tailwind classes merged with the component's CVA classes via cn().
childrenReact.ReactNode-Item content. Text for default mode, a nested element (e.g. <a>) when asChild is true.

Variants

VariantOptionsDefaultDescription
variant"primary" "neutral" "soft"inherits from ButtonGroupOverride the group's variant for a single item. Rarely needed.
size"xs" "sm" "md" "lg" "xl"inherits from ButtonGroupOverride the group's size for a single item. Rarely needed.
iconOnlybooleanfalseSquare footprint; drops the label slot and centers a single icon.