Toggle

A two-state button that can be toggled on or off. Built on the @base-ui/react Toggle primitive with CVA variants for visual style and responsive sizing. Ideal for binary choices like bold/unbold, mute/unmute, or favorite/unfavorite.

Anatomy

Each toggle is a single <Toggle>element backed by Base UI's headless Toggle primitive. It manages its own pressed state via aria-pressed and exposes two variant axes: variant (visual style) and size (dimensions). The pressed state is reflected through both aria-pressed and data-[state=on] attributes.

aria-pressed="true"data-slot="toggle"data-[state=on]

Variants

Five visual variants control the toggle's appearance. The pressed state applies bg-muted text-foreground on default/ghost/outline variants, and variant-specific styles on secondary and destructive.

variant="default"

Transparent background, muted pressed state. The standard choice for toolbars.

variant="outline"

Visible border in resting state. Use when toggles need to stand out from the background.

variant="secondary"

Filled background with secondary color. For toggles that should be visually prominent.

variant="ghost"

No visible background or border until hover/press. Ideal for dense icon grids.

variant="destructive"

Red-tinted for toggles that control dangerous states (e.g., disabling a safety feature).

Responsive Sizes

Three text sizes and one icon-only size, each scaling across breakpoints. The size system mirrors the Button component for consistency.

TokenPreviewResponsive HeightsWhen to Use
size="sm"h-7 / sm:h-8 / lg:h-9Compact toolbars, inline controls
size="default"h-8 / sm:h-9 / lg:h-10Standard toggle actions
size="lg"h-10 / sm:h-11 / lg:h-12Prominent standalone toggles
size="icon"size-8 / sm:size-9 / lg:size-10Square icon-only toggles

Side-by-side comparison

Common Patterns

Toggles are most useful for binary state controls. Here are typical usage patterns in real interfaces.

Favorite / Bookmark

Mute / Visibility

With text label

States

Toggles cycle between resting (off), pressed (on), focused, and disabled. The pressed state is communicated via aria-pressed="true" and visual background change.

Off (resting)

On (pressed)

focus-visible

disabled, 50% opacity

Token Map

Quick reference mapping each size token to its generated Tailwind classes across the three breakpoint tiers.

SizeMobile (base)Tablet (sm:)Desktop (lg:)
smh-7 min-w-7 px-1.5h-8 min-w-8 px-2h-9 min-w-9 px-2.5
defaulth-8 min-w-8 px-2h-9 min-w-9 px-2.5h-10 min-w-10 px-3
lgh-10 min-w-10 px-3 text-baseh-11 min-w-11 px-3.5h-12 min-w-12 px-4
iconsize-8size-9size-10

Design Guidelines

Do

  • Always provide aria-label. Icon-only toggles have no visible text — the label is required for screen readers to announce the toggle purpose.
  • Make the on/off states visually distinct. The pressed state should be immediately obvious. The built-in background change handles this, but consider adding icon swaps (e.g., eye/eye-off) for extra clarity.
  • Use for binary choices only. Toggle is for on/off. For selecting among multiple options, use ToggleGroup instead.
  • Group related toggles visually. Place related toggles adjacent with consistent spacing. For tighter grouping with shared borders, use ToggleGroup.

Don't

  • Don't use toggle for navigation or one-shot actions. Toggle represents state, not actions. Use Button for actions like "Save" or "Submit".
  • Don't forget to indicate the current state. Relying solely on subtle color changes can be missed. Pair with icon changes or text labels when the toggle has significant consequences.
  • Don't disable without context. A disabled toggle with no explanation leaves users confused. Add a tooltip explaining why the toggle is unavailable.
  • Don't mix toggle sizes in the same toolbar. Inconsistent sizes create visual noise. Use the same size prop for all toggles in a related group.

Developer Reference

Accessibility

  • The underlying primitive manages aria-pressedautomatically. Screen readers announce "Toggle bold, pressed" or "Toggle bold, not pressed".
  • Focus ring uses focus-visible — keyboard-only, not on mouse click.
  • active:translate-y-px provides a physical press cue complementing the visual state change.
  • disabled sets pointer-events: none and 50% opacity. The element stays in the tab order; screen readers announce it as disabled.
  • Pressed state is reflected via both aria-pressed and data-[state=on], supporting both ARIA queries and CSS selectors.

Usage

import { Toggle } from "@/components/ui/toggle"
import { Bold, Italic, Star } from "lucide-react"

// Basic toggle (default variant)
<Toggle aria-label="Toggle bold">
  <Bold className="size-4" />
</Toggle>

// Outline variant
<Toggle variant="outline" aria-label="Toggle italic">
  <Italic className="size-4" />
</Toggle>

// With text label
<Toggle variant="outline" aria-label="Toggle bold">
  <Bold className="size-4" />
  Bold
</Toggle>

// Controlled state
const [pressed, setPressed] = useState(false)
<Toggle
  pressed={pressed}
  onPressedChange={setPressed}
  aria-label="Favorite"
>
  <Star className="size-4" />
</Toggle>

// Sizes — responsive scaling via Tailwind
<Toggle size="sm" aria-label="Small">...</Toggle>   {/* h-7 → h-8 → h-9 */}
<Toggle aria-label="Default">...</Toggle>            {/* h-8 → h-9 → h-10 */}
<Toggle size="lg" aria-label="Large">...</Toggle>   {/* h-10 → h-11 → h-12 */}
<Toggle size="icon" aria-label="Icon">...</Toggle>  {/* size-8 → size-9 → size-10 */}

// Variants
<Toggle variant="default">...</Toggle>
<Toggle variant="outline">...</Toggle>
<Toggle variant="secondary">...</Toggle>
<Toggle variant="ghost">...</Toggle>
<Toggle variant="destructive">...</Toggle>