Skip to content

Toggle

Use a toggle when the button is the state — bold, mute, pin, favorite — 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 color 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 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. Left unset, size comes from the enclosing ToggleGroup, and failing that from a Toolbar.Root around it, so a toggle in a sm bar is sm without being told.

toggle/sizes.tsx

Icon only

iconOnly squares the button and keeps it on the control height: the space around the icon, --forte-toggle-icon-padding, is derived from the height and --forte-toggle-icon-size, so a sm toggle is 28px beside a 28px button rather than growing past it, and the square is still pinned to at least 24px so the hit target clears WCAG SC 2.5.8. A direct svg child is sized from that same knob — 14px, 16px, 18px by size — the way Button sizes one, so an icon set's own default does not decide the square. An icon bigger than the knob says 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 color. 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 colors, 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
KeyBehavior
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 colors 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-icon-sizeSize of an svg a consumer drops in; reassigned by size. Direct children only, so a composed forte-ui component keeps sizing its own glyphs.var(--forte-font-size-3)
--forte-toggle-border-widthBorder width, drawn in every variant — outline is the only one that gives it a color.1px
--forte-toggle-durationColor 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 color while unpressed.var(--forte-color-foreground-muted)
--forte-toggle-fg-hoverLabel color on hover while unpressed.var(--forte-color-foreground)
--forte-toggle-border-colorBorder color 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-hover)
--forte-toggle-bg-pressed-hoverFill on hover while pressed. Set by variant from the tone.var(--forte-toggle-tone-soft-active)
--forte-toggle-fg-pressedLabel color while pressed. Set by variant from the tone.var(--forte-toggle-tone-text)
--forte-toggle-fg-pressed-hoverLabel color on hover while pressed — the tone's high-contrast text, which is what lets the fill deepen a step without the label dropping below AA. solid holds its on-fill color instead.var(--forte-toggle-tone-text-strong)
--forte-toggle-border-color-pressedBorder color while pressed. The tone's border for variant="outline".transparent
--forte-toggle-icon-paddingSpace between the icon and the toggle's edge (iconOnly only). Derived from the height and --forte-toggle-icon-size so the square lands on the control height; set a length to pad a larger icon instead.max( 0px, calc( (var(--forte-toggle-height) - var(--forte-toggle-icon-size)) / 2 - var(--forte-toggle-border-width) ) )

tone reassigns the nine --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 -hover values only ever apply on devices that report hover support, so a touch device never sticks in a hover color after a tap.

Hovering a pressed toggle deepens the label along with the fill — the tone's -text-strong in place of its -text. That pairing is not decoration: the resting fill already sits at step 4, so its hover moves to step 5, and the tone's ordinary text tier does not clear AA there (4.17:1 at the worst seed in the contrast sweep, against 8.33:1 for the high-contrast tier). solid keeps its on-fill label, having no tint to deepen.

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, and failing that from a `Toolbar.Root` around it — a toggle handed to `Toolbar.Button` through `render` needs no size of its own.
toneToggleToneprimaryWhich semantic color 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.