Toggle
Turn a pressed state on or off, alone or within a related group.
Overview
Use Toggle for a button whose on/off state is visible in the surrounding content: text formatting, layer visibility, mute controls, view options. The state is the point — pressing changes something the person can see right where they are.
Use Switch instead when the control reads as a setting that applies elsewhere, and Button for actions that run once. A toggle is a question of state ("is bold on?"), not an order ("do this now").
Anatomy
A single toggle is a native <button> wrapping a content span that carries the pressed surface treatment. Styling reacts to a data-pressed attribute, and the underlying Base UI primitive always emits aria-pressed, so assistive technology hears "pressed" or "not pressed" without you writing any ARIA yourself.
A toggle group is a container (role="group") holding several toggles plus optional separators. Inside a group, items inherit the group's variant and size through context and lose their own borders and radius so they read as one segmented control.
Pressed state
Standalone toggles accept pressed / defaultPressed and report onPressedChange(pressed, eventDetails). In a group, each item gets a value; the group holds an array of currently pressed values and exposes value / defaultValue / onValueChange(groupValue, eventDetails) for controlled use.
Single mode (the default, multiple={false}) keeps at most one item pressed — but empty is allowed. Pressing the pressed item clears it, which makes it a filter, not a radio group.
Multiple mode (multiple) lets any combination stay pressed independently, like formatting buttons.
Sizes and variants
Sizes map to the design system's space scale: "1" is 12 px tall, "2" is 16 px, "3" is 20 px, and "4" is 24 px. The named aliases are sm (matches "3" at 20 px) and default (24 px) — and lg is currently identical in height to default, a quirk we document rather than hide, so don't reach for lg expecting a bigger control. The outline variant swaps the filled surface for a bordered one; inside a group it outlines the whole container instead of each item.
Accessibility
Keyboard follows native button behavior: Tab reaches a standalone toggle and Enter/Space press it. Focus rings appear only for keyboard focus.
In a group, Base UI implements roving focus: only one item is in the tab order, and ←→ (horizontal groups) or ↑↓ (vertical, orientation="vertical") move focus between items, looping by default (loopFocus). Pressing still uses Enter/Space, and each press updates every item's aria-pressed.
Icon-only toggles have no text content, so give each one an aria-label. Because pressed state is also conveyed visually through background and inset shadow — not color alone — the state survives color-vision differences. Colors come from theme tokens, so dark mode needs no extra work, and inline-flex layout mirrors automatically in right-to-left locales. Long labels grow the button rather than wrap.
Target sizes are honest about their limits: sizes "1"–"3" render 12–20 px tall, below the WCAG 2.2 minimum target of 24 × 24 px. Reserve them for dense desktop toolbars paired with tooltips; use default or larger on touch surfaces.
Installation
Usage
import { Toggle, ToggleGroup } from "@/components/ui/toggle";<Toggle defaultPressed>Bold</Toggle><ToggleGroup defaultValue={["bold"]} multiple>
<Toggle value="bold" aria-label="Bold">
<BoldIcon />
</Toggle>
<Toggle value="italic" aria-label="Italic">
<ItalicIcon />
</Toggle>
</ToggleGroup>ToggleGroupItem is an alias of Toggle, and toggleVariants is exported for composing custom toggle-styled elements.
Don't do this
Hand-rolling a toggle from a plain button
// Bad
const [on, setOn] = useState(false);
<Button variant={on ? "secondary" : "ghost"} onClick={() => setOn(!on)}>
Bold
</Button>// Good
<Toggle pressed={on} onPressedChange={setOn}>Bold</Toggle>The hand-rolled version announces nothing: no aria-pressed, no state change for screen readers — people activate it and hear silence. It also loses the disabled-cursor handling and keyboard guarantees the primitive provides. Base UI's Toggle already emits aria-pressed on every render; let it.
Treating a single-selection group as a radio choice
// Bad
<ToggleGroup value={plan} onValueChange={setPlan} aria-label="Plan">
<Toggle value="starter">Starter</Toggle>
<Toggle value="pro">Pro</Toggle>
</ToggleGroup>// Good
<RadioGroup value={plan} onValueChange={setPlan} aria-label="Plan">
<Radio value="starter">Starter</Radio>
<Radio value="pro">Pro</Radio>
</RadioGroup>Single mode permits an empty group — clicking the pressed item unpresses it. That is correct for filters ("clear this facet") and wrong for required choices like plan selection, where "no answer" must not be one click away. Use a RadioGroup when exactly one option must always hold.
Icon-only toggles without names
// Bad
<Toggle value="bold"><BoldIcon /></Toggle>// Good
<Toggle value="bold" aria-label="Bold">
<BoldIcon aria-hidden="true" />
</Toggle>An unnamed icon-only toggle reads as just "toggle button, not pressed" — which button? Voice-control users have nothing meaningful to say, and screen-reader users guess from position alone. Name every icon-only toggle; mark its icon decorative.
Examples
View mode switcher
A single-selection group where exactly one view can be active. Remember the empty-selection caveat: if an unpressed group would break your grid, enforce a value in onValueChange.
Multiple selection
Independent pressed values, like formatting controls.
Outline variant with separators
The outline style draws one border around the whole group; separators divide items that need visual distinction.
One disabled item
A group where one choice is temporarily unavailable; the rest keep working.
Icon labels with tooltips
Tooltips explain icon-only toggles on hover and focus, while aria-label remains the accessible name.
API reference
Toggle accepts Base UI Toggle props plus Honest UI additions:
Disabled toggles intentionally keep pointer-events: auto so hovering shows a not-allowed cursor even though clicks do nothing. The rendered element is a native <button>; type cannot be changed, and the component never participates in form validation.
ToggleGroup accepts Base UI Toggle Group props:
ToggleGroupSeparator renders a vertical separator between items. See the Base UI Toggle API and Toggle Group API.