Skip to main content

ToggleButton

A toggle button component that supports pressed/unpressed states with optional icon morphing and label animations.

Import

import { ToggleButton } from "@soft-ui/react/toggle-button"

Props

icon
React.ReactNode
required
Icon to display when not pressed.
pressedIcon
React.ReactNode
Icon to display when pressed. Falls back to icon if not provided.
children
React.ReactNode
Label to display when not pressed.
pressedChildren
React.ReactNode
Label to display when pressed. Falls back to children if not provided.
variant
string
default:"tertiary"
The visual style variant.Options:
  • tertiary - Glass-like appearance with shadow and blur (default)
  • ghost - Transparent background, visible on hover
  • secondary - Subtle gray background
size
string
default:"m"
The size of the button.Options:
  • xs - 28px height/width (var(—space-28))
  • s - 32px height/width (var(—space-32))
  • m - 36px height/width (var(—space-36))
  • l - 40px height/width (var(—space-40))
pressedTone
string
Color tone applied when pressed.Feedback tones:
  • default, info, warning, danger, success
Decorative tones:
  • red, orange, amber, yellow, lime, green, emerald
  • teal, cyan, sky, blue, indigo, violet, purple
  • fuchsia, pink, rose
morph
boolean
default:false
Enable morphing animation between icons (blur, scale, y-position). When false, uses a smooth fade transition.
pressed
boolean
Controlled pressed state. When provided, the component becomes controlled.
defaultPressed
boolean
Default pressed state for uncontrolled usage.
onPressedChange
(pressed: boolean, eventDetails: object) => void
Callback fired when the pressed state changes.
labelWidth
number
Fixed width in pixels for label animation (unpressed state). Enables smooth width transitions.
pressedLabelWidth
number
Fixed width in pixels for label animation (pressed state).
disabled
boolean
When true, disables interaction and applies disabled styling.
className
string
Additional CSS classes to apply to the button.
unsafeClassName
string
Explicit escape hatch for intentional structural overrides.

Usage

Icon-Only Toggle

import { HeartIcon, HeartFilledIcon } from "@soft-ui/icons"

<ToggleButton
  icon={<HeartIcon />}
  pressedIcon={<HeartFilledIcon />}
  aria-label="Like"
/>

With Label

import { BookmarkIcon, BookmarkFilledIcon } from "@soft-ui/icons"

<ToggleButton
  icon={<BookmarkIcon />}
  pressedIcon={<BookmarkFilledIcon />}
>
  Save
</ToggleButton>

Variants

import { StarIcon, StarFilledIcon } from "@soft-ui/icons"

<ToggleButton
  variant="tertiary"
  icon={<StarIcon />}
  pressedIcon={<StarFilledIcon />}
  aria-label="Favorite"
/>
<ToggleButton
  variant="ghost"
  icon={<StarIcon />}
  pressedIcon={<StarFilledIcon />}
  aria-label="Favorite"
/>
<ToggleButton
  variant="secondary"
  icon={<StarIcon />}
  pressedIcon={<StarFilledIcon />}
  aria-label="Favorite"
/>

Sizes

import { BellIcon } from "@soft-ui/icons"

<ToggleButton size="xs" icon={<BellIcon />} aria-label="Notifications" />
<ToggleButton size="s" icon={<BellIcon />} aria-label="Notifications" />
<ToggleButton size="m" icon={<BellIcon />} aria-label="Notifications" />
<ToggleButton size="l" icon={<BellIcon />} aria-label="Notifications" />

With Pressed Tone

import { HeartIcon, HeartFilledIcon } from "@soft-ui/icons"

<ToggleButton
  icon={<HeartIcon />}
  pressedIcon={<HeartFilledIcon />}
  pressedTone="danger"
  aria-label="Like"
/>

Icon Morph Animation

import { SunIcon, MoonIcon } from "@soft-ui/icons"

<ToggleButton
  icon={<SunIcon />}
  pressedIcon={<MoonIcon />}
  morph
  aria-label="Toggle theme"
/>

Changing Labels

import { BookmarkIcon, BookmarkFilledIcon } from "@soft-ui/icons"

<ToggleButton
  icon={<BookmarkIcon />}
  pressedIcon={<BookmarkFilledIcon />}
  pressedChildren="Saved"
>
  Save
</ToggleButton>

Animated Label Width

import { BookmarkIcon, BookmarkFilledIcon } from "@soft-ui/icons"

<ToggleButton
  icon={<BookmarkIcon />}
  pressedIcon={<BookmarkFilledIcon />}
  labelWidth={40}
  pressedLabelWidth={55}
  pressedChildren="Saved"
>
  Save
</ToggleButton>

Controlled Toggle

import { useState } from "react"
import { MicIcon, MicOffIcon } from "@soft-ui/icons"

function AudioControls() {
  const [muted, setMuted] = useState(false)
  
  return (
    <ToggleButton
      icon={<MicIcon />}
      pressedIcon={<MicOffIcon />}
      pressed={muted}
      onPressedChange={setMuted}
      aria-label="Toggle mute"
    />
  )
}

Uncontrolled with Default

import { StarIcon, StarFilledIcon } from "@soft-ui/icons"

<ToggleButton
  icon={<StarIcon />}
  pressedIcon={<StarFilledIcon />}
  defaultPressed={true}
  aria-label="Favorite"
/>

Animation

ToggleButton includes two animation modes:

Fade Mode (default)

Smooth opacity transition between icons:
  • Duration: 150ms
  • Easing: ease-out
  • No morphing effects

Morph Mode

Dynamic icon transformation with:
  • Y-axis translation: 8px
  • Scale: 0.5 to 1.0
  • Blur: 0 to 8px
  • Spring animation: bounce 0.2, duration 250ms
Label animations use spring physics (bounce 0.15, duration 250ms) when labelWidth is provided. All animations respect prefers-reduced-motion and become instant transitions.

Accessibility

  • Built on Base UI’s Toggle primitive with ARIA support
  • When using icon-only, always provide aria-label
  • Focus ring uses utility-focus-inner and utility-focus-outer
  • Active state includes subtle scale animation (0.98)
  • Pressed state is communicated via aria-pressed attribute
  • Disabled state uses cursor-not-allowed and reduced opacity

Design Tokens

The ToggleButton component uses the following design tokens: Colors:
  • actions-tertiary-default, actions-tertiary-hover, actions-tertiary-disabled
  • actions-secondary-default, actions-secondary-hover, actions-secondary-disabled
  • content-strong, content-subtle, content-disabled
Spacing:
  • Heights/widths: space-28, space-32, space-36, space-40
  • Horizontal padding: space-10, space-12, space-16
  • Gap between elements: space-2, space-4
  • Icon sizes: space-16, space-18
  • Label padding: space-4, space-6
Other:
  • Border radius: radius-max
  • Font weight: font-weight-medium
  • Focus ring: utility-focus-inner, utility-focus-outer
  • Shadows (tertiary): utility-shadow-l1, utility-shadow-l2, utility-shadow-l3

Build docs developers (and LLMs) love