Toggle
A single-button two-state press control. Renders a real <button> with aria-pressed and data-state="on" | "off" — use it for "this one action is on or off" affordances like bold, italic, mute-icon, favourite.
Customize
Style
default
md
Installation
npx shadcn@latest add https://craftbits.dev/r/toggle.jsonUsage
import { Toggle } from "@craft-bits/core";
<Toggle aria-label="Toggle bold">Bold</Toggle>Controlled — pair pressed with onPressedChange:
const [bold, setBold] = useState(false);
<Toggle pressed={bold} onPressedChange={setBold} aria-label="Toggle bold">
<BoldIcon />
</Toggle>Uncontrolled — let the component own its state via defaultPressed:
<Toggle defaultPressed aria-label="Toggle italic">
<ItalicIcon />
</Toggle>Understanding the component
- Single press button — not a group.
Toggleis the "one action, on/off" primitive. For a segmented multi-option control, reach forTogglePill(compound) orModeToggle(props-driven). The three siblings cover the full toggle space. - Controlled + uncontrolled.
pressed+onPressedChangemakes it a controlled mirror of your state;defaultPressedlets the component manage its own. Same shape as Radix'svalue/defaultValuepattern. - State hooks. The button exposes
aria-pressed(announced by screen readers) anddata-state="on" | "off"(used by Tailwind variants). Thedata-[state=on]:bg-cb-bg-muted data-[state=on]:text-cb-fgrule paints the pressed surface; override it viaclassNameif you need a different active treatment. - Tap feedback. Press scale uses the canonical
TAP_SCALEfrom@craft-bits/core/motion; the transition runs onSPRINGS.snapso it composes with the rest of the transport family. Disabled buttons skip both — there's nothing to react to. - Variant + size matrix.
defaultis a borderless surface (toolbar use);outlineadds a neutral border for flat backgrounds. Thesm/md/lgsizes follow the same32 / 36 / 40 pxramp as the other transport controls so the button drops into a row ofPlayPause,SoundToggle, etc. without vertical drift.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
pressed | boolean | — | Controlled pressed state. Pair with onPressedChange. |
defaultPressed | boolean | false | Uncontrolled initial pressed state. |
onPressedChange | (pressed: boolean) => void | — | Fired with the next pressed state when the user clicks. |
variant | "default" | "outline" | "default" | Visual treatment. outline adds a neutral border. |
size | "sm" | "md" | "lg" | "md" | Height + min-width: 32 / 36 / 40 px. |
disabled | boolean | false | Disables the button — no click, no tap feedback. |
aria-label | string | — | Accessible name when the button only renders an icon. |
className | string | — | Merged onto the rendered <button> via cn(). |
...rest | ButtonHTMLAttributes<HTMLButtonElement> | — | Any other <button> prop is spread onto the root. |
Accessibility
- Renders a real
<button type="button">so it picks up native focus, keyboard activation (Space / Enter), and disabled semantics for free. aria-pressedreflects the current state on every render so assistive tech announces transitions correctly.data-state="on" | "off"mirrorsaria-pressedfor CSS styling hooks (data-[state=on]:…in Tailwind).- Provide
aria-labelwhen the button only contains an icon — the icon SVG should bearia-hidden. With text children, the visible label is the accessible name. - Focus is visible via a
focus-visible:ring keyed to--cb-accentagainst a--cb-bgoffset, so keyboard users always see the current target. - Tap scale respects
prefers-reduced-motion: Motion's reduced-motion handling collapses the transition to an instant snap.
Credits
- Extracted from:
AlgoFlashcards(src/platform/ui/toggle.tsx). The source wrappedradix-ui'sToggle.Root; craft-bits drops the extra dependency, hand-rolls the controlled-state plumbing the same waySoundToggledoes, and adds theTAP_SCALE+SPRINGS.snapfeel so the button composes with the rest of the transport family.