formsclient

toggle

a switch for settings that should take effect right away.

auto-save drafts

installation

new to blurrow? this sets up your project in one go: the package, the styles and the font. already done it? skip straight to usage. the installation page has the details.

bash
npx @blurrow/ui init
›rather own the code? copy the source instead

let the cli copy it, along with anything it needs:

bash
npx @blurrow/ui add toggle

or by hand: drop this file into components/ui and import from there.

components/ui/Toggle.tsxtsx
"use client";
import React, { useId, useState } from "react";
interface ToggleProps {
checked?: boolean;
defaultChecked?: boolean;
onChange?: (checked: boolean) => void;
label?: string;
description?: string;
disabled?: boolean;
}
export function Toggle({
checked,
defaultChecked = false,
onChange,
label,
description,
disabled = false,
}: ToggleProps) {
const [internal, setInternal] = useState(defaultChecked);
const [pressed, setPressed] = useState(false);
const uid = useId();
const isOn = checked !== undefined ? checked : internal;
const toggle = () => {
if (disabled) return;
const next = !isOn;
setInternal(next);
onChange?.(next);
};
return (
<div
className={[
"flex items-center justify-between gap-4 select-none",
disabled ? "opacity-35 pointer-events-none" : "cursor-pointer",
].join(" ")}
onClick={toggle}
>
{(label || description) && (
<div className="flex flex-col gap-0.5 min-w-0">
{label && (
<span id={`${uid}-label`} className="text-[13px] font-medium text-neutral-900 leading-snug">
{label}
</span>
)}
{description && (
<span id={`${uid}-desc`} className="text-[12px] text-neutral-500 leading-relaxed">
{description}
</span>
)}
</div>
)}
{/* Clicks bubble to the row, so the switch itself has no onClick */}
<button
type="button"
role="switch"
aria-checked={isOn}
aria-labelledby={label ? `${uid}-label` : undefined}
aria-describedby={description ? `${uid}-desc` : undefined}
disabled={disabled}
onPointerDown={() => setPressed(true)}
onPointerUp={() => setPressed(false)}
onPointerLeave={() => setPressed(false)}
className={[
"relative shrink-0 w-[46px] h-[26px] rounded-[13px] cursor-pointer",
"transition-[background-color,box-shadow] duration-200 ease-out",
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-900",
isOn ? "bg-neutral-900" : "bg-neutral-200/70",
].join(" ")}
style={{
/* The track is a recessed channel; the knob is a raised face inside it */
boxShadow: isOn
? "inset 0 1px 3px rgba(0,0,0,0.5), inset 0 0 2px rgba(0,0,0,0.3), 0 1px 2px -1px rgba(255,255,255,0.48)"
: "inset 0 1px 2px rgba(17,17,17,0.12), inset 0 0 2px rgba(17,17,17,0.12), 0 1px 2px -1px rgba(255,255,255,0.64)",
}}
>
<span
aria-hidden
className="absolute top-1/2 h-5 rounded-[10px]"
style={{
left: 3,
/* The knob stretches while held, like a finger dragging it */
width: pressed ? 24 : 20,
background: "var(--face-light)",
boxShadow:
"0 0 3px rgba(17,17,17,0.119), 0 1px 2px rgba(0,0,0,0.14), 0 3px 6px -1px rgba(0,0,0,0.12), inset 0 2px 3px -2px rgba(255,255,255,1), inset 0 -2px 3px -2px rgba(0,0,0,0.04)",
transform: `translateX(${isOn ? (pressed ? 16 : 20) : 0}px) translateY(-50%)`,
transition:
"transform 260ms var(--ease-spring), width 200ms var(--ease-out-soft)",
}}
/>
</button>
</div>
);
}

usage

tsx
import { Toggle } from "@blurrow/ui";
tsx
<Toggle label="email notifications" checked={on} onChange={setOn} />

examples

with description

email notificationsreceive the weekly digest
push notificationscomments and mentions

disabled

scheduled publishingupgrade to pro to unlock

props

straight from the source, so they're always up to date.

propsts
interface ToggleProps {
checked?: boolean;
defaultChecked?: boolean;
onChange?: (checked: boolean) => void;
label?: string;
description?: string;
disabled?: boolean;
}