bx

Toggle Group

A set of two-state buttons that can be toggled on or off.

tsx
<ToggleGroup ariaLabel="Text formatting">
  <ToggleGroupItem value="bold" pressed ariaLabel="Bold"><b aria-hidden="true">B</b></ToggleGroupItem>
  <ToggleGroupItem value="italic" ariaLabel="Italic"><i aria-hidden="true">I</i></ToggleGroupItem>
  <ToggleGroupItem value="underline" ariaLabel="Underline"><u aria-hidden="true">U</u></ToggleGroupItem>
</ToggleGroup>

ToggleGroup is a row of toggle buttons that belong together (text formatting, alignment, a view switch). With type="multiple" (the default) every item toggles on its own. With type="single" at most one item is on: pressing an item releases the one that was on, and pressing the item that is on releases it.

Installation

Terminal
$ bext ui add toggle-group

Usage

tsx
import { ToggleGroup, ToggleGroupItem } from "@bext-stack/ui/primitives"

<ToggleGroup type="single" ariaLabel="Text alignment">
  <ToggleGroupItem value="left" pressed>Left</ToggleGroupItem>
  <ToggleGroupItem value="center">Center</ToggleGroupItem>
  <ToggleGroupItem value="right">Right</ToggleGroupItem>
</ToggleGroup>

Anatomy

<ToggleGroup type="single" ariaLabel="…">
  ├── <ToggleGroupItem value="a" pressed>A</ToggleGroupItem>
  └── <ToggleGroupItem value="b">B</ToggleGroupItem>

API Reference

ToggleGroup

Prop Type Default Description
type "single" | "multiple" "multiple" "multiple": items are independent. "single": at most one item is on. Written to data-type and enforced by the island.
ariaLabel string — Accessible name of the group (aria-label).
ariaLabelledby string — id of a visible element that names the group.
variant "default" | "outline" "default" "outline" draws a border around each item.
spacing 0 | 1 | 2 0 Gap between items. With 0 the items are joined and share their corners.
size "default" | "sm" | "lg" "default" Written to data-size on the group.
id string — id of the group.
className string — Extra classes on the group.

ToggleGroupItem

Prop Type Default Description
value string — Written to data-value on the button.
pressed boolean false Initial state. In a single group, set it on one item only.
ariaLabel string — aria-label of the button, for an icon-only item.
disabled boolean false Disables the item: it is skipped by the arrow keys.
id string — id of the button.
children ReactNode — Content of the button.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-ui="toggle-group" ToggleGroup data-type="single" | "multiple" Scope of the group for the island.
data-ui="toggle" ToggleGroupItem data-value="…" Clicking flips the item.
data-state ToggleGroupItem "on" | "off" Current state. The pressed look is styled from it.
aria-pressed ToggleGroupItem (multiple) "true" | "false" Same state, for assistive technologies.
aria-checked ToggleGroupItem (single) "true" | "false" Replaces aria-pressed once the island has turned the group into a radio group.

Keyboard

Key Action
Tab / Shift+Tab Moves focus into the group (onto the item that is on, else the first one), then out of it. The group is a single tab stop.
Arrow Right / Arrow Down Moves focus to the next item. Wraps to the first. Does not change the state.
Arrow Left / Arrow Up Moves focus to the previous item. Wraps to the last.
Home / End Moves focus to the first or last item.
Space / Enter Presses or releases the focused item.

Accessibility

The group renders role="group" and each item is a <button type="button"> with aria-pressed, kept in sync by the island. Name the group with ariaLabel or ariaLabelledby, and give icon-only items an ariaLabel.

With type="single" the island exposes the group as a radio set: the root becomes role="radiogroup" and the items role="radio" with aria-checked instead of aria-pressed. Unlike a RadioGroup, the arrow keys only move the focus, and the item that is on can be released, which leaves the group with no selection. When a value is always required, use RadioGroup.

The island keeps a roving tabindex on the items. Without JavaScript every item stays in the tab order and the group stays a plain role="group" of pressed buttons.

Examples

Single

tsx
<ToggleGroup type="single" variant="outline" ariaLabel="Text alignment">
  <ToggleGroupItem value="left" pressed>Left</ToggleGroupItem>
  <ToggleGroupItem value="center">Center</ToggleGroupItem>
  <ToggleGroupItem value="right">Right</ToggleGroupItem>
</ToggleGroup>
Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.