Toggle
A two-state button that persists its pressed/active state. Visually reflects on/off state and supports controlled and uncontrolled modes.
Install
$
npx react-principles add toggle01
Features
- ✓Visual State: Distinct pressed/unpressed styling
- ✓Controlled & Uncontrolled: Use
pressed+onPressedChangeordefaultPressed - ✓Accessible: Correct
aria-pressedattribute - ✓Keyboard Accessible: Space/Enter to toggle
- ✓Variants & Sizes:
default/outlinein sm/md/lg
02
Live Demo
Toolbar (controlled)
Active: none
Default variant
Sizes
03
Code Snippet
Toggle.tsx
import { Toggle } from "@/ui/Toggle"; function Toolbar() { const [bold, setBold] = useState(false); const [italic, setItalic] = useState(false); return ( <div className="flex gap-1"> <Toggle pressed={bold} onPressedChange={setBold}> B </Toggle> <Toggle pressed={italic} onPressedChange={setItalic}> I </Toggle> </div> ); }
04
Copy-Paste (Single File)
Toggle.tsx
import { useState, type ButtonHTMLAttributes, type ReactNode } from "react"; import { cn } from "@/shared/utils/cn"; export type ToggleVariant = "default" | "outline"; export type ToggleSize = "sm" | "md" | "lg"; export interface ToggleProps extends ButtonHTMLAttributes<HTMLButtonElement> { pressed?: boolean; defaultPressed?: boolean; onPressedChange?: (pressed: boolean) => void; variant?: ToggleVariant; size?: ToggleSize; disabled?: boolean; children: ReactNode; } const VARIANT_CLASSES: Record<ToggleVariant, Record<"on" | "off", string>> = { default: { on: "bg-primary text-white hover:bg-primary/90 focus-visible:ring-primary/40", off: "bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700 focus-visible:ring-slate-400/40", }, outline: { on: "bg-slate-100 text-slate-900 border border-slate-300 dark:bg-slate-800 dark:text-white dark:border-slate-600 focus-visible:ring-slate-400/40", off: "border border-slate-300 text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-800/50 focus-visible:ring-slate-400/40", }, }; const SIZE_CLASSES: Record<ToggleSize, string> = { sm: "text-xs px-3 py-1.5 h-7 gap-1.5", md: "text-sm px-4 py-2 h-9 gap-2", lg: "text-base px-6 py-2.5 h-11 gap-2", }; export function Toggle({ pressed, defaultPressed = false, onPressedChange, variant = "default", size = "md", disabled = false, children, className, onClick, ...props }: ToggleProps) { const [internal, setInternal] = useState(defaultPressed); const isControlled = pressed !== undefined; const isOn = isControlled ? pressed : internal; const toggle = () => { if (disabled) return; const next = !isOn; if (!isControlled) setInternal(next); onPressedChange?.(next); }; return ( <button type="button" aria-pressed={isOn} disabled={disabled} onClick={(e) => { onClick?.(e); toggle(); }} className={cn( "inline-flex items-center justify-center font-semibold rounded-lg transition-all", "focus-visible:outline-hidden focus-visible:ring-2", "disabled:opacity-50 disabled:cursor-not-allowed", isOn ? VARIANT_CLASSES[variant].on : VARIANT_CLASSES[variant].off, SIZE_CLASSES[size], className, )} {...props} > {children} </button> ); }
05
Usage Examples
Uncontrolled
<Toggle defaultPressed>Toggle</Toggle>
Controlled
const [isOn, setIsOn] = useState(false); <Toggle pressed={isOn} onPressedChange={setIsOn}> Toggle </Toggle>
Outline variant with sizes
<div className="flex gap-3"> <Toggle variant="outline" size="sm">Small</Toggle> <Toggle variant="outline" size="md">Medium</Toggle> <Toggle variant="outline" size="lg">Large</Toggle> </div>
06
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| pressed | boolean | — | Controlled pressed state |
| defaultPressed | boolean | false | Uncontrolled initial pressed state |
| onPressedChange | (pressed: boolean) => void | — | Called when pressed state changes |
| variant | "default" | "outline" | "default" | Visual variant |
| size | "sm" | "md" | "lg" | "md" | Button size |
| disabled | boolean | false | Disable the toggle |