DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ButtonGroupSplit ButtonToggleButtonToggleGroupFAB
AI agentsContributing
  1. Components
  2. ToggleButton

ToggleButton

A button that toggles between on and off states, with the same variant, intent, size, and rounded system as Button. Shows a persistent pressed visual when active.

$ pnpm add @tessinaui/ui

When to use

  • A state that applies immediately: bold in a toolbar, mute, a filter toggled on and off. If the change needs saving, it is a form control, not a toggle.
  • A control that has to look like a button beside other buttons, with the same variant, intent, size and rounded as Button.
  • An icon-only toggle in a toolbar: size="icon" with an aria-label.
  • A label that stays the same in both states. "Star" that becomes "Starred" changes width mid-click and makes the pressed state harder, not easier, to read. The state is already carried by aria-pressed and the fill.
  • Short async work before the state settles: set loading and the toggle keeps its width and its focus.

When not to use

  • For a persistent on/off setting. Use Switch: a toggle button reads as "apply this now", a switch as "this is how it stays".
  • For one action that does something and has no state. Use Button.
  • For one-of-many. Use ToggleGroup or SegmentedControl, which manage selection and roving focus for you.
  • For a choice that needs saving or confirming. Use a Checkbox in a form.
  • To navigate. Use Link or Tabs.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { ToggleButton } from "@tessinaui/ui";
example.tsx
<ToggleButton>Star</ToggleButton>

Examples

Default

A controlled toggle. The label stays the same in both states; the fill and aria-pressed carry the state.

Variants

Four visual styles — primary, secondary, ghost, and outline.

Sizes

Five text sizes from xs to xl on the family scale (32 / 40 / 48 / 56 / 96 px), plus icon — a 48 px square for an icon-only toggle that requires an accessible name. xs and sm keep a 44 px minimum touch target on phones and tablets.

Intents

Semantic color intents — none, error, warning, success, and info.

States

Off, pressed, disabled, and loading states.

On color

Use tone="on-color" so toggles read correctly on a colored surface.

Skeleton

RTL

dir="rtl" puts leadingIcon on the right of the label and trailingIcon on the left. Unset, dir is inherited from the page, and the prop wins when set.

API Reference

Props

PropTypeDefaultDescription
pressedboolean—Controlled pressed state
defaultPressedbooleanfalseInitial pressed state for uncontrolled usage
onPressedChange(pressed: boolean) => void—Callback fired when pressed state changes
variant"primary" | "secondary" | "ghost" | "outline""primary"Visual style
intent"none" | "error" | "warning" | "success" | "info""none"Semantic color intent
size"xs" | "sm" | "md" | "lg" | "xl" | "icon""md"Family scale 32 / 40 / 48 / 56 / 96 px; icon is a 48 px square and requires aria-label or aria-labelledby (enforced by the type)
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Corner radius
tone"default" | "on-color"inherited from <Surface>High-contrast styling for a coloured or dark surface; pressed becomes the inverse chip
fullWidthbooleanfalseStretch to the container width
showBorderbooleantrueoutline only. outline means "border at rest", as on Button; pass false for a row of toggles that should read as one quiet surface until one is on — or use variant="ghost", which is what that look is
leadingIconReact.ReactNode—Icon rendered before the label. Stays mounted while loading (the spinner overlays it) so the width never moves
trailingIconReact.ReactNode—Icon rendered after the label. Stays mounted while loading
loadingbooleanfalseOverlays a spinner, sets aria-busy + aria-disabled, swallows activation. Keeps focus and size; not natively disabled
disabledbooleanfalseDisables the button and prevents toggling
dir"ltr" | "rtl"—Text direction — RTL reverses icon/label order via CSS flex. Unset, it inherits the page's direction

Controlled vs Uncontrolled

Controlled — manage pressed state externally:

example.tsx
const [starred, setStarred] = useState(false);

<ToggleButton pressed={starred} onPressedChange={setStarred}>
  Star
</ToggleButton>

Uncontrolled — let the component manage its own state:

example.tsx
<ToggleButton defaultPressed={false} onPressedChange={(p) => console.log(p)}>
  Star
</ToggleButton>

Pressed intents

Accessibility

VoiceOver + Safari: passed 2026-09-14. Focus reads "Star, toggle button". Pressing it is silent, as a native <button aria-pressed> is in VoiceOver + Safari; re-reading it adds "selected" between the label and "toggle button". NVDA and JAWS are untested.

Follows the WAI-ARIA Button pattern, toggle variant.

  • Renders a <button> with aria-pressed reflecting the current toggle state
  • data-pressed is present (empty string) when pressed and absent when not — target it as [data-pressed], not [data-pressed="true"]. A [data-pressed="false"] selector never matches anything
  • Keyboard accessible: Enter and Space toggle the button
  • Focus ring visible with 2px ring + 2px offset (matches design system standard)
  • Disabled state prevents toggling and is reflected via the native disabled attribute
  • loading is not the same as disabled: the toggle keeps focus and reports aria-busy, and both icons stay mounted so its width does not move
  • Under Windows high-contrast mode the pressed state maps to the system Highlight colours — the painted fill is stripped there, which would otherwise leave an icon-only toggle with no pressed indication at all
  • size="icon" requires aria-label or aria-labelledby, enforced by the type
  • Touch targets ≥ 44×44px on phones and tablets (xs/sm grow; md+ already clear it)
Split ButtonToggleGroup

On this page

When to useWhen not to useInstallationUsageExamplesDefaultVariantsSizesIntentsStatesOn colorSkeletonRTLAPI ReferencePropsControlled vs UncontrolledPressed intentsAccessibility