Skip to content

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.

toggle/basic.tsx

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.

toggle/variants.tsx
solidsoftoutline

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.

toggle/tones.tsx

Sizes

The same three steps as Button1.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.

toggle/sizes.tsx

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.

toggle/icon-only.tsx

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.

toggle/favorite.tsx

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.

toggle/controlled.tsx
const summary = rows.filter((r) => r.status === "open").map((r) => r.title).join(", ");

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.

toggle/disabled.tsx

Accessibility

Keyboard interactions
KeyBehaviour
TabMoves focus to the toggle. Inside a ToggleGroup the whole group is one tab stop — see that page.
Space then EnterFlips 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.

Theming tokens for Toggle
PropertyControlsDefault
--forte-toggle-radiusCorner radius.var(--forte-radius-control)
--forte-toggle-heightHeight. size re-points it.var(--forte-control-h-md)
--forte-toggle-padding-xInline padding. iconOnly replaces it with --forte-toggle-icon-padding on all four sides.var(--forte-control-px-md)
--forte-toggle-font-sizeLabel size.var(--forte-font-size-2)
--forte-toggle-font-weightLabel weight.var(--forte-font-weight-medium)
--forte-toggle-gapGap between an icon and its label.var(--forte-control-gap)
--forte-toggle-border-widthBorder width, drawn in every variant — outline is the only one that gives it a colour.1px
--forte-toggle-durationColour transitions between states.var(--forte-duration-fast)
--forte-toggle-press-scaleHow 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-bgFill while unpressed.transparent
--forte-toggle-bg-hoverFill on hover while unpressed.var(--forte-color-panel-hover)
--forte-toggle-fgLabel colour while unpressed.var(--forte-color-foreground-muted)
--forte-toggle-fg-hoverLabel colour on hover while unpressed.var(--forte-color-foreground)
--forte-toggle-border-colorBorder colour while unpressed. var(--forte-color-border) for variant="outline".transparent
--forte-toggle-bg-pressedFill while pressed. Set by variant from the tone.var(--forte-toggle-tone-soft)
--forte-toggle-bg-pressed-hoverFill on hover while pressed. Set by variant from the tone.var(--forte-toggle-tone-soft-hover)
--forte-toggle-fg-pressedLabel colour while pressed. Set by variant from the tone.var(--forte-toggle-tone-text)
--forte-toggle-border-color-pressedBorder colour while pressed. The tone's border for variant="outline".transparent
--forte-toggle-icon-paddingMinimum 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

Props for Toggle
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
iconOnlybooleanfalseRender 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.
refRef<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`.
sizeToggleSizemdSize of the button. Matches `Button` step for step so the two line up in one toolbar. Inherited from an enclosing `ToggleGroup` when left unset.
toneToggleToneprimaryWhich semantic colour set the pressed state draws from. Combines freely with `variant`. Inherited from an enclosing `ToggleGroup` when left unset.
valuestringA 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.
variantToggleVariantsoftHow 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.