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

ToggleGroup

A group of toggle buttons — a multi-select toolbar set or a single-select switcher — built on Base UI's ToggleGroup primitive with aria-pressed semantics.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { ToggleGroup, ToggleGroupItem } from "@tessinaui/ui";
<ToggleGroup aria-label="Alignment" defaultValue={["left"]}>
  <ToggleGroupItem value="left" leadingIcon={<AlignLeft />} label="Left" />
  <ToggleGroupItem value="center" leadingIcon={<AlignCenter />} label="Center" />
  <ToggleGroupItem value="right" leadingIcon={<AlignRight />} label="Right" />
</ToggleGroup>

When to use which

Most "toggle group"-looking UI is actually one of five components. The name collides with its neighbours, so pick by semantics, not by looks:

You wantUse
A set of on/off states — independent toggles (bold/italic/underline), or a switcher where deselecting is legal or the spaced-pill / ghost skin is wantedToggleGroup
Exactly-one-of-N with the sliding thumb-on-track look and radio semanticsSegmentedControl
A scrollable row of filter choices with checkmarks and overflowChip / ChipGroup
Switching between panels of distinct content, possibly URL-addressableTabs
A cluster of independent actions (buttons that do things, not states)ButtonGroup

Two rules the survey systems agree on: toggle-group items are in-page state, never links (Primer states it outright — navigation belongs to Tabs), and a group is all-labelled or all-icon, never mixed (Carbon, Primer, HIG).

Examples

Default

A single-select alignment group with the default outline variant.

Variants

Two container styles — outline (filled background) and ghost (transparent).

Sizes

Five labelled sizes from xs to xl, plus square icon.

Icon only

size="icon" renders square, icon-only items — the toolbar view-switcher pattern. Every item must carry its own aria-label, and an item with no visible text of its own shows that name as a tooltip on hover and keyboard focus — automatically, because a glyph alone is not self-explanatory (Primer and Spectrum both require a tip for exactly this case). Pass tooltip to override the text, or tooltip={false} to opt out.

Multiple Selection

With multiple, each item toggles independently like a set of checkboxes.

Required selection

requireSelection keeps single-select groups from ever being empty — pressing the only selected item is a no-op. This is the view-switcher contract (React Aria's disallowEmptySelection; M3 requires a selection in segmented buttons).

Full width

fullWidth stretches the track to its container; labelled items share the width equally — the mobile fulfilment/billing pattern.

Intents

All five house intents tint the pressed state.

Rounded

The radius cascade, including the derived stadium a vertical full group takes from its own item height.

Rich items

Items accept arbitrary children — a label with a sublabel, or a badge beside the text.

Do not reach for text-muted-foreground in a sublabel here. An item sits on the group's own secondary track, and muted ink composites to 3.2–4.0:1 there (measured, both themes) — under AA. Use text-foreground/75, the same step Accordion, BottomNav and Breadcrumb took for the identical trap.

Days of week

Multi-select single-character cells (the repeat-schedule pattern) as a flush track — seven 44px touch targets plus gaps cannot fit a 320px viewport, so gap="none" is the phone-safe shape. The visible letter is ambiguous, so each item's accessible name is the full day.

Time range

A dense ghost group for chart ranges — only the pressed item paints.

Toolbar composition

A multiple format group beside a requireSelection alignment group — two groups, two accessible names, one visual row.

Vertical

Stack items vertically with orientation="vertical".

States

A loading item, plus a fully disabled group.

Skeleton

API Reference

ToggleGroup Props

PropTypeDefaultDescription
variant"outline" | "ghost""outline"Visual style applied to all items
intent"none" | "error" | "warning" | "success" | "info""none"Semantic color intent applied to all items
size"xs" | "sm" | "md" | "lg" | "xl" | "icon""md"Size applied to all items; icon renders square icon-only items
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Corner radius for the container and every item; vertical full derives a stadium
orientation"horizontal" | "vertical""horizontal"Layout direction of the group
gap"micro" | "none""micro"Gap between items — micro is 4px (horizontal) / 8px (vertical), none is 0px
fullWidthbooleanfalseStretch the track to its container; labelled items share the width equally
multiplebooleanfalseWhen true, multiple items can be pressed at once; when false, at most one item is pressed
requireSelectionbooleanfalseSingle-select only: the last pressed item cannot be toggled off. Ignored (with a dev warning) under multiple
valuestring[]—Controlled array of pressed item values
defaultValuestring[][]Initial pressed values for uncontrolled usage
onValueChange(value: string[], eventDetails) => void—Fired when the pressed set changes; never fired with [] while requireSelection holds
loopFocusbooleantrueWhether arrow-key focus wraps around the group
disabledbooleanfalseDisables every item in the group at once
dir"ltr" | "rtl"—Unset, the group follows the document. When set it is stamped on the root and provided through Base UI's DirectionProvider, so arrow-key order actually flips

ToggleGroupItem Props

PropTypeDefaultDescription
valuestring—Required. Identifier used in the group's value array when the item is pressed
labelstring—Visible label text
childrenReact.ReactNode—Rich content (sublabel, badge…). Wins over label when both are given
leadingIconReact.ReactNode—Icon before the label; stays mounted while loading (the spinner overlays it)
trailingIconReact.ReactNode—Icon after the label; stays mounted while loading
loadingbooleanfalseShows a spinner, sets aria-busy, and refuses activation — the item stays focusable
disabledbooleanfalseDisables this item individually
tooltipReact.ReactNode | false—Hover/focus tip. An icon-only item (no label, no children) shows its aria-label automatically; pass a node to override, or false to opt out
aria-labelstring—Accessible name — required for every item in a size="icon" group

Items also forward arbitrary HTML attributes (data-*, aria-describedby, test ids) to the rendered <button>.

Single vs Multiple

By default ToggleGroup is single-select (multiple={false}) — pressing a new item unpresses the previous one, and pressing the current item deselects it (the group may be empty). Add requireSelection when a view must always be chosen. The value prop is an array in both modes so they share one shape:

// Single select, never empty — a view switcher
const [view, setView] = useState<string[]>(["list"]);

<ToggleGroup requireSelection value={view} onValueChange={setView} aria-label="View">
  <ToggleGroupItem value="list" leadingIcon={<List />} aria-label="List" />
  <ToggleGroupItem value="map"  leadingIcon={<MapTrifold />} aria-label="Map" />
</ToggleGroup>
// Multi select — each item toggles independently
const [format, setFormat] = useState<string[]>([]);

<ToggleGroup multiple value={format} onValueChange={setFormat} aria-label="Formatting">
  <ToggleGroupItem value="bold"      leadingIcon={<Bold />} aria-label="Bold" />
  <ToggleGroupItem value="italic"    leadingIcon={<Italic />} aria-label="Italic" />
  <ToggleGroupItem value="underline" leadingIcon={<Underline />} aria-label="Underline" />
</ToggleGroup>

Supported Combinations

VariantIntents
outlinenone, error, warning, success, info
ghostnone, error, warning, success, info

Sizes

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 ToggleGroup, a SegmentedControl and a Stepper sitting side by side read as one family. Container height is therefore the item height plus twice that inset.

SizeItem heightContainer heightUse case
xs32px36pxCompact toolbars
sm40px44pxSecondary actions
md48px56pxDefault, most use cases
lg56px64pxProminent toolbars
xl96px104pxHero / large controls
icon48×48px56pxIcon-only view switchers, day cells

On phones, xs and sm items grow to the 44px touch floor, and a vertical rounded="full" group's stadium follows that height.

Accessibility

The ToggleGroup component:

  • Built on Base UI's ToggleGroup + Toggle primitives
  • Renders a role="group" container — give it an accessible name via aria-label / aria-labelledby (the component dev-warns without one)
  • Each item is a <button aria-pressed> in both modes — never role="radiogroup"/aria-checked. This is the unanimous model across Base UI, Radix and React Aria; Carbon's tablist-flavoured content switcher is the documented outlier, and that shape belongs to Tabs/SegmentedControl here
  • Item labels never change with pressed state (the APG toggle-button rule); selection is conveyed by aria-pressed and the pressed fill — never by swapping icons (Primer's rule)
  • An icon-only item's tooltip is a sighted affordance on top of the accessible name it already carries — it opens on keyboard focus as well as hover, and it never becomes the name, so the button announces identically with or without it. Touch has no hover: the aria-label is what carries the meaning there, which is why it stays required
  • Keyboard: Tab enters the group (one stop — roving tabindex), Arrow keys move focus (wrapping via loopFocus), Enter / Space toggle
  • RTL: set dir="rtl" on the group — the attribute alone is not enough for Base UI, so the component also provides direction context, flipping arrow-key order along with the layout
  • loading sets aria-busy and refuses activation while keeping the item focusable — never native disabled mid-interaction
  • data-slot="toggle-group" / data-slot="toggle-group-item", data-variant, data-size, data-gap, data-full-width, data-pressed expose deterministic hooks for CSS, tests and agents
  • Focus ring meets WCAG 2.1 AA on each item (2px ring + 2px offset); touch targets are ≥44×44px on phones for xs/sm, larger sizes exceed the floor
ToggleButtonTokenizer

On this page

PlaygroundInstallationUsageWhen to use whichExamplesDefaultVariantsSizesIcon onlyMultiple SelectionRequired selectionFull widthIntentsRoundedRich itemsDays of weekTime rangeToolbar compositionVerticalStatesSkeletonAPI ReferenceToggleGroup PropsToggleGroupItem PropsSingle vs MultipleSupported CombinationsSizesAccessibility