Toggle
A two-state button whose icon cross-fades between states, with the standard press.
Installation
$ pnpm dlx shadcn@latest add @ssntl/toggleUsage
import { Toggle } from "@/components/ui/toggle"
<Toggle aria-label="Save product" off={<Bookmark />} on={<BookmarkCheck />} />
<Toggle off={<Bell />} on={<BellRing />}>Follow updates</Toggle>Rules
What this component expects from you, as it ships in components/ui/toggle.md next to the code. The full file adds when to use it, keyboard, the UI foundations it applies and every shared rule.
## Rules for Toggle
- **Toggle and selection feedback.** The standard 0.96 press in 150ms, never a slow or deep squash.
- The icon cross-fades between its two states; `aria-pressed` carries the state for screen readers.
- An icon-only toggle needs an `aria-label` that names the action, not the state.Code
"use client"
import * as React from "react"
import { Toggle as TogglePrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/registry/ssntl/ui/button"
// A two-state button (Radix Toggle, aria-pressed): save, follow, pin.
// Toggle and selection feedback: the same 0.96 press in 150ms as every
// button, and the icon cross-fades between its two states (icon-swap), so
// the change reads instantly and the state stays in the icon and aria-pressed.
function Toggle({
className,
children,
on,
off,
pressed,
defaultPressed = false,
onPressedChange,
...props
}: Omit<React.ComponentProps<typeof TogglePrimitive.Root>, "children"> & {
/** The icon while pressed. */
on: React.ReactNode
/** The icon while not pressed. */
off: React.ReactNode
/** A visible label; without one, pass aria-label. */
children?: React.ReactNode
}) {
const [inner, setInner] = React.useState(defaultPressed)
const current = pressed ?? inner
return (
<TogglePrimitive.Root
data-slot="toggle"
pressed={current}
onPressedChange={(next) => {
setInner(next)
onPressedChange?.(next)
}}
className={cn(
buttonVariants({ variant: "ghost", size: children ? "md" : "icon" }),
!children && "px-0",
"data-[state=on]:text-foreground",
className
)}
{...props}
>
<span
className="icon-swap"
data-swapped={current}
data-icon={children ? "inline-start" : undefined}
>
{off}
{on}
</span>
{children}
</TogglePrimitive.Root>
)
}
export { Toggle }