Toggle
Use a toggle when the button is the state — bold, mute, pin, favourite — and
the effect is immediate and local. It is a <button> with aria-pressed, not a
form control: it has no label of its own and submits nothing.
If the state is a setting the user confirms later, you want
Switch or Checkbox instead —
both carry a visible label and a form value, and a toggle button has neither. If
the buttons are alternatives rather than independent states, reach for
ToggleGroup.
Import
import { Toggle } from "@forte-ui/react";Examples
Variants
variant decides how loud the pressed state is. Unpressed is quiet in every
variant — that is what makes a toggle read as off at a glance — so the variant
only describes what "on" looks like: solid fills with the tone, soft tints
with it, outline tints and keeps a border that is drawn while unpressed too.
Tones
tone picks the semantic colour set the pressed state draws from, and combines
freely with variant. It only reaches the pressed state: an unpressed toggle
looks the same in all four tones.
Sizes
The same three steps as Button — 1.75rem, 2.25rem, 2.75rem — so a toggle
dropped into a row of buttons lines up rather than sitting a pixel proud. All
three follow the ambient data-forte-density setting.
Icon only
iconOnly squares the button, gives the icon --forte-toggle-icon-padding of
space on every side, and pins the square to at least the control height and
24px, so the hit target still clears WCAG SC 2.5.8 at size="sm". An icon too
big for that floor grows the button instead of crowding the corners.
Swapping the icon with the state
The render callback receives the toggle's state, which is what lets the
icon itself change rather than only its colour. This is Base UI's own example,
styled with our tokens.
Two different shapes — outline and filled — mean the state survives greyscale, a monochrome theme and forced colours, none of which a hue swap would. Reach for the callback form whenever a toggle's meaning is carried by an icon.
Controlled
Pass pressed and onPressedChange for a controlled toggle; defaultPressed
for an uncontrolled one. The callback receives the new state first and Base UI's
event details second — call details.cancel() on it to veto the change.
Disabled
disabled blocks pointer and keyboard interaction and sets aria-disabled,
keeping whichever state the toggle is in. On a ToggleGroup it reaches every
toggle inside.
Accessibility
| Key | Behaviour |
|---|---|
| Tab | Moves focus to the toggle. Inside a ToggleGroup the whole group is one tab stop — see that page. |
| Space then Enter | Flips the pressed state. Both keys work, as on any button. |
The root renders a native <button> carrying aria-pressed, so assistive
technology announces it as a toggle button and reads the state out with the
name. That is the whole reason to prefer this over a <button> whose label
changes: the name stays stable and only the state moves.
Under forced colours a pressed toggle paints Highlight with HighlightText,
an unpressed one Canvas with CanvasText, and a disabled one GrayText on
Canvas whichever state it holds. This needs a rule of its own in the component
rather than the library-wide one in patterns.css: that rule covers
data-checked, aria-checked and aria-selected, and a toggle button is none
of those — it is aria-pressed. Without it, on and off would look identical in
high contrast.
Motion. The only geometry is the press squash, --forte-scale-press, which
collapses to exactly 1 under prefers-reduced-motion: reduce on its own — so
there is no media query in the component. Press runs on
--forte-duration-instant and the release on --forte-ease-spring-snappy: snap in,
spring out. There is deliberately no hover lift or hover scale slot, unlike
Button. A toggle is usually one of several in a strip, and a single item
growing under the pointer breaks the shared edge that makes the strip read as
one control.
Theming
Every visual decision is a custom property declared on the toggle root. Set them
on the toggle itself — through className or an inline style — rather than on
an ancestor: the component declares its own defaults on the root element, and an
element's own declaration beats an inherited value.
| Property | Controls | Default |
|---|---|---|
--forte-toggle-radius | Corner radius. | var(--forte-radius-control) |
--forte-toggle-height | Height. size re-points it. | var(--forte-control-h-md) |
--forte-toggle-padding-x | Inline padding. iconOnly replaces it with --forte-toggle-icon-padding on all four sides. | var(--forte-control-px-md) |
--forte-toggle-font-size | Label size. | var(--forte-font-size-2) |
--forte-toggle-font-weight | Label weight. | var(--forte-font-weight-medium) |
--forte-toggle-gap | Gap between an icon and its label. | var(--forte-control-gap) |
--forte-toggle-border-width | Border width, drawn in every variant — outline is the only one that gives it a colour. | 1px |
--forte-toggle-duration | Colour transitions between states. | var(--forte-duration-fast) |
--forte-toggle-press-scale | How far the button squashes while held. Multiplied by --forte-motion-ok, so a literal value still collapses under reduced motion. | var(--forte-scale-press) |
--forte-toggle-bg | Fill while unpressed. | transparent |
--forte-toggle-bg-hover | Fill on hover while unpressed. | var(--forte-color-panel-hover) |
--forte-toggle-fg | Label colour while unpressed. | var(--forte-color-foreground-muted) |
--forte-toggle-fg-hover | Label colour on hover while unpressed. | var(--forte-color-foreground) |
--forte-toggle-border-color | Border colour while unpressed. var(--forte-color-border) for variant="outline". | transparent |
--forte-toggle-bg-pressed | Fill while pressed. Set by variant from the tone. | var(--forte-toggle-tone-soft) |
--forte-toggle-bg-pressed-hover | Fill on hover while pressed. Set by variant from the tone. | var(--forte-toggle-tone-soft-hover) |
--forte-toggle-fg-pressed | Label colour while pressed. Set by variant from the tone. | var(--forte-toggle-tone-text) |
--forte-toggle-border-color-pressed | Border colour while pressed. The tone's border for variant="outline". | transparent |
--forte-toggle-icon-padding | Minimum space between the icon and the toggle's edge (iconOnly only) | var(--forte-space-2) |
tone reassigns the seven --forte-toggle-tone-* properties the variants read
from, so overriding one of those re-skins every variant at once, while
overriding a -pressed slot changes only the variant in play.
The three -hover values only ever apply on devices that report hover support,
so a touch device never sticks in a hover colour after a tap.
API reference
Toggle
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
iconOnly | boolean | false | Render as a square button sized for a single icon, holding the 24×24 minimum hit target from WCAG SC 2.5.8. Always pair with `aria-label` — an icon is not an accessible name. |
ref | Ref<HTMLButtonElement> | Ref to the button element. Declared as a prop rather than through `forwardRef` because the component is generic — a `forwardRef` wrapper would erase `Value` and with it the inference on `value`. | |
size | ToggleSize | md | Size of the button. Matches `Button` step for step so the two line up in one toolbar. Inherited from an enclosing `ToggleGroup` when left unset. |
tone | ToggleTone | primary | Which semantic colour set the pressed state draws from. Combines freely with `variant`. Inherited from an enclosing `ToggleGroup` when left unset. |
value | string | A unique string identifying this toggle within a `ToggleGroup`. Required inside a group — the group's value is the list of pressed toggles' values, so a toggle without one can never appear in it. Ignored outside a group, where `pressed` / `defaultPressed` carry the state instead. | |
variant | ToggleVariant | soft | How loud the *pressed* state is. Unpressed is quiet in every variant — that is what makes a toggle read as off — so the variant only decides what "on" looks like: `solid` fills with the tone, `soft` tints with it, `outline` tints and keeps a border that is also drawn while unpressed. There is deliberately no `ghost`: every variant is already chromeless at rest, so a ghost toggle would differ from `soft` in nothing but its pressed fill — and dropping that fill would leave hue as the only cue separating on from off. Inherited from an enclosing `ToggleGroup` when left unset. |
Anything Base UI's Toggle accepts passes through as well — including
pressed, defaultPressed, onPressedChange, disabled, nativeButton and
render.