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.
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.
| Token | Preview | Responsive Heights | When to Use |
|---|---|---|---|
size="sm" | h-7 / sm:h-8 / lg:h-9 | Compact toolbars, inline controls | |
size="default" | h-8 / sm:h-9 / lg:h-10 | Standard toggle actions | |
size="lg" | h-10 / sm:h-11 / lg:h-12 | Prominent standalone toggles | |
size="icon" | size-8 / sm:size-9 / lg:size-10 | Square 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.
| Size | Mobile (base) | Tablet (sm:) | Desktop (lg:) |
|---|---|---|---|
sm | h-7 min-w-7 px-1.5 | h-8 min-w-8 px-2 | h-9 min-w-9 px-2.5 |
default | h-8 min-w-8 px-2 | h-9 min-w-9 px-2.5 | h-10 min-w-10 px-3 |
lg | h-10 min-w-10 px-3 text-base | h-11 min-w-11 px-3.5 | h-12 min-w-12 px-4 |
icon | size-8 | size-9 | size-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-pxprovides a physical press cue complementing the visual state change.disabledsetspointer-events: noneand 50% opacity. The element stays in the tab order; screen readers announce it as disabled.- Pressed state is reflected via both
aria-pressedanddata-[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>