DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

ButtonGroup

A cluster of related but independent actions — in a shared pill container or attached into one connected control, horizontal or vertical

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { ButtonGroup, ButtonGroupItem } from "@tessinaui/ui";
<ButtonGroup variant="outline" size="md" aria-label="Clipboard actions">
  <ButtonGroupItem leadingIcon={<Scissors />} label="Cut" />
  <ButtonGroupItem leadingIcon={<Copy />} label="Copy" />
  <ButtonGroupItem leadingIcon={<Clipboard />} label="Paste" />
</ButtonGroup>

Every item is an independent action with its own tab stop. Name the group — aria-label, or aria-labelledby pointing at a visible heading.

When to use which

ButtonGroup is for independent actions that happen to belong together. Nothing in it is selected, and pressing one does not change the state of the others. If that is not your case, one of its neighbours fits better:

You wantUseWhy
Related actions in one cluster — Cut / Copy / Paste, a toolbarButtonGroupEach item does something; none is "on"
One exclusive choice — Day / Week / Month, Light / Auto / DarkSegmentedControlA selection track with a moving thumb and radio semantics
A set of on/off states — Bold / Italic / UnderlineToggleGroupItems are pressed or not, and stay that way
Filter or answer chips — pick a category, pick up to two tagsChip / ChipGroupSelection chips, spaced, wrapping
One primary action plus related alternativesSplitButtonThe menu belongs to the button
A dense, persistent action bar with one tab stopToolbarRoving arrow-key focus, which a group deliberately does not do

The name is the trap: most interfaces that look like a "button group" are actually a segmented control or a row of choice chips.

Examples

Default

A horizontal group of pill buttons with leading icons and labels.

Variants

Two visual styles — outline with a container background and ghost with a transparent one.

Intents

All five house intents are available, on both variants and both tones. Intent tints the item's ink and its state layers — it never changes the container, so a group stays one object.

A group is usually neutral with a single item carrying the intent, which is how every surveyed product renders a destructive action among its siblings. Set it on the group only when the whole cluster shares one meaning.

An item that collapses into the overflow menu keeps error but renders neutral for the other intents — menu rows carry none | error only.

Attached

Set attached to collapse the items into one connected control: no plate, no gap, inner corners flattened and a single hairline seam between neighbours. This is the shape most design systems ship as their only button group; here it is opt-in, and the pill container stays the default.

An outline group collapses the doubled border where two items meet. A ghost group has no border to collapse, so it gains a hairline — without one, two attached items on the same fill have no visible boundary at all.

Sizes

Five sizes from xs to xl scale height, padding, and icon footprint together — the same scale Button uses, so a md group lines up with a md button.

Rounded

Six steps from none to full. The container's corner sits one token above the items' so they stay tucked inside it, and a vertical pill derives its corner from the item height rather than rounding to an oval.

Vertical

Set orientation="vertical" to stack items into a column — the shape of a canvas-tool rail or a narrow side panel.

Full width

fullWidth stretches the group to its container. Only the container's sizing changes — items stay equal-width and still shrink rather than overflow.

Text cell

ButtonGroupText puts a non-interactive readout inside the run — a zoom level, a unit, a count. It takes the same box as an item so the run keeps one height, and differentiates by having no states at all: no hover, no press, no focus ring. It is not a tab stop, and its text is read as part of the group.

Separator

ButtonGroupSeparator divides a run into clusters without splitting the container. It is decorative and draws the same AA-gated hairline the attached seam uses. An attached group already draws its own seams, so a separator there paints a second line — it is for capsule and ghost runs.

Overflow

overflow="collapse" moves whatever does not fit into a trailing More actions menu, in order, instead of letting the run shrink. The group measures the space its container gives it, so the cut follows the real layout rather than a breakpoint. Nothing is lost: every hidden action is in the menu, by name.

A loading item collapses like any other and lands disabled — a busy action is not invocable, and keeping it pinned would reorder the strip mid-interaction.

Responsive stacking

stackOn="auto" turns the group into a column when its container is too narrow for the run — a phone, and equally a narrow desktop column, which a breakpoint cannot see. The group stamps data-stacked and reports the orientation it is actually in, so styles and tests can key on the truth.

Before hydration it renders as a row and shrinks, exactly as it does today; the stack lands in a layout effect, before paint. stackOn and overflow are mutually exclusive — collapsing exists precisely to avoid reflowing the run.

Icon only

Drop the label and provide an aria-label for compact icon-only toolbars.

On a coloured surface

Inside a Surface, items pick up tone="on-color" automatically and paint from the surface's own ink instead of the page palette.

States

A loading item, a single disabled item, and a fully disabled group. A loading item stays focusable and reports aria-busy — it is busy, not unavailable.

Skeleton

ButtonGroupSkeleton takes its box from the same styles as the real group, so the placeholder cannot drift from the control it stands in for.

API Reference

ButtonGroup Props

PropTypeDefaultDescription
variant"outline" | "ghost""outline"Visual weight applied to all items
intent"none" | "error" | "warning" | "success" | "info""none"Semantic meaning applied to all items. A single destructive action usually sets intent on the item instead
size"xs" | "sm" | "md" | "lg" | "xl""md"Size applied to all items
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Corner radius; items follow the container's silhouette
tone"default" | "on-color"inheritedWhether the group sits on a coloured surface. Inherited from a parent Surface when unset
orientation"horizontal" | "vertical""horizontal"Layout direction of the group
attachedbooleanfalseCollapse the items into one connected control — no plate, no gap, flattened inner corners, one hairline seam
fullWidthbooleanfalseStretch the container to its parent; items stay equal-width
overflow"collapse"—Move whatever does not fit into a trailing "More actions" menu. Horizontal only; mutually exclusive with stackOn
overflowLabelstring"More actions"Accessible name for the overflow trigger
stackOn"never" | "auto""never"auto stacks the group into a column when its container is too narrow. Horizontal only
gap"micro" | "none""micro"Gap between items — micro is 4px horizontally, 8px vertically. Ignored while attached
showItemBorderbooleanfalseDraw a visible border on every item. Ignored while attached, where the border is the seam
disabledbooleanfalseDisables all items in the group at once
dir"ltr" | "rtl""ltr"Text direction — RTL reverses item and icon order

ButtonGroupItem Props

PropTypeDefaultDescription
labelstring—Visible label text
leadingIconReact.ReactNode—Icon rendered before the label
trailingIconReact.ReactNode—Icon rendered after the label
variant"outline" | "ghost"group'sOverrides the group's weight for this item alone
intent"none" | "error" | "warning" | "success" | "info"group'sOverrides the group's intent for this item alone — how a destructive action sits among neutral siblings
loadingbooleanfalseShows a spinner over the label. The item stays focusable and reports aria-busy; activation is suppressed
disabledbooleanfalseDisables this item individually
type"button" | "submit" | "reset""button"Defaults to button, so an item inside a form cannot submit it by accident
aria-labelstring—Accessible label (required when no visible label is provided)
onClickMouseEventHandler—Click handler

ButtonGroupText Props

A non-interactive cell. Takes children plus any div attribute; height, padding, type scale, radius and border all come from the group.

ButtonGroupSeparator Props

Decorative divider. Takes any div attribute; its axis follows the group's orientation.

ButtonGroupSkeleton mirrors the group's size, variant, orientation, gap, rounded, attached and fullWidth, plus count, labels, showLeadingIcon and showTrailingIcon.

Variants

Variant options

VariantUse
outlineDefault. Items sit transparent on the group's container fill
ghostNo container fill; items paint only on hover and press

Intent options

All five house intents are available on the group and per item. Intent tints the item's ink and its state layers; it never changes the container.

IntentUse
noneDefault. Neutral actions
errorDestructive — delete, remove, revoke
warningActions with a caveat
successConfirming, approving
infoInformational

Size options

SizeItem heightContainer heightUse case
xs32px (44px on phones)36pxCompact toolbars
sm40px (44px on phones)44pxSecondary actions
md48px56pxDefault, most use cases
lg56px64pxProminent toolbars
xl96px104pxHero / large controls

xs and sm sit below the 44×44px WCAG 2.5.5 touch floor, so they grow to it on phones and tablets and keep the compact desktop density from md up. These are the same heights Button uses, so the two line up when placed side by side.

Every grouped control insets its items from the container edge by the same shared scale — 2px at xs/sm, 4px from md up — so a ButtonGroup, a ToggleGroup, a SegmentedControl and a Stepper sitting side by side read as one family. Container height is the item height plus twice that inset; an attached group has no inset, so its height is the item height.

Rounded options

RoundedItemContainer
noneSquareSquare
sm4px6px
md6px8px
lg8px12px
xl12px16px
fullPillPill

The container's corner clears the item's by the inset it holds, so items never read as rectangles floating inside a rounded plate. A vertical full group derives its corner from the item height instead of clamping to its own width, which would round a tall column into an oval. While attached there is no inset, so the container and the run's outer corners are the same radius.

Accessibility

  • The wrapper renders role="group". Name it with aria-label, or aria-labelledby pointing at a visible heading.
  • Every item is its own tab stop. These are independent actions, so there is no roving arrow-key focus — that contract belongs to Toolbar, and taking it on would oblige arrow-key navigation the group does not implement.
  • Icon-only items require aria-label; the icon itself is hidden from assistive technology.
  • The focus ring is the house geometry (ring-2 plus a 2px offset in the surface colour) and lifts above its neighbours so it is never clipped — including in attached, where the wrapper opens a stacking context for exactly that reason.
  • Touch targets are ≥44×44px on phones and tablets at every size.
  • loading keeps the item focusable and sets aria-busy with aria-disabled rather than the native disabled attribute — a control that natively disables itself mid-interaction throws focus to the page body. Clicks and the activation keys are suppressed; Escape still reaches your handler. The label stays mounted, so the item keeps its accessible name while busy.
  • disabled (item or group) uses the native attribute and removes the item from the tab order. When both are set, disabled wins.
  • An attached ghost group draws a hairline between items in the AA-gated outline token, so neighbouring controls are always distinguishable.
  • RTL is fully supported: attached corners use logical properties, so a horizontal run mirrors without a second set of styles.
  • A ButtonGroupText cell is not focusable and has no role — it is read as part of the group's content, which is what a zoom level or a unit should be.
  • ButtonGroupSeparator is aria-hidden; grouping is visual, and the group's own accessible name carries the meaning.
  • Under overflow="collapse" no action is lost. Whatever does not fit moves, by name, into a menu opened from a named More actions button — reachable by keyboard like any other menu. Rename it with overflowLabel when the group is one of several on a page, or to localise it.
ButtonCalendar

On this page

PlaygroundInstallationUsageWhen to use whichExamplesDefaultVariantsIntentsAttachedSizesRoundedVerticalFull widthText cellSeparatorOverflowResponsive stackingIcon onlyOn a coloured surfaceStatesSkeletonAPI ReferenceButtonGroup PropsButtonGroupItem PropsButtonGroupText PropsButtonGroupSeparator PropsVariantsVariant optionsIntent optionsSize optionsRounded optionsAccessibility