Forms
ToggleCraft
A switch built on a real checkbox. The knob stretches while pressed like a soft bead, then springs across while colour spreads out from under it.
Download
- React
- Tailwind CSS v4
- motion
components/ui/toggle-craft.tsx
"use client";
import { useState, type CSSProperties, type InputHTMLAttributes } from "react";
import { motion, useReducedMotion } from "motion/react";
type ToggleCraftProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "checked" | "defaultChecked" | "onChange" | "className"> & {
/** Text next to the switch. */
label: string;
/** Small text under the label. */
hint?: string;
/** On or off (controlled). */
checked?: boolean;
/** On or off at start (uncontrolled). */
defaultChecked?: boolean;
/** Called with the new state. */
onChange?: (checked: boolean) => void;
/** Colour of the on state. */
accent?: string;
/** Wrapper look (width etc.). */
className?: string;
};
/* ToggleCraft: a switch built on a real checkbox. The knob stretches while pressed like a soft bead, then springs across while colour spreads out from under it. */
export function ToggleCraft({
label,
hint,
checked,
defaultChecked = false,
onChange,
accent = "#a78bfa",
className = "w-full max-w-xs",
...rest
}: ToggleCraftProps) {
const reduce = useReducedMotion();
const [inner, setInner] = useState(defaultChecked);
const [pressed, setPressed] = useState(false);
const on = checked ?? inner;
const stretch = pressed && !reduce ? 6 : 0;
const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 520, damping: 30 };
return (
<label
style={{ "--tc": accent } as CSSProperties}
onPointerDown={() => setPressed(true)}
onPointerUp={() => setPressed(false)}
onPointerLeave={() => setPressed(false)}
onPointerCancel={() => setPressed(false)}
className={`flex cursor-pointer select-none items-center justify-between gap-4 has-disabled:cursor-not-allowed has-disabled:opacity-50 ${className}`}
>
<span className="min-w-0">
<span className="block text-[15px] font-medium text-white">{label}</span>
{hint && <span className="mt-0.5 block text-[13px] text-white/50">{hint}</span>}
</span>
<span className="relative shrink-0">
<input
type="checkbox"
role="switch"
checked={on}
onChange={(e) => {
if (checked === undefined) setInner(e.target.checked);
onChange?.(e.target.checked);
}}
className="peer sr-only"
{...rest}
/>
<span className="relative block h-8 w-14 overflow-hidden rounded-full bg-white/12 transition-shadow duration-150 peer-focus-visible:ring-2 peer-focus-visible:ring-[var(--tc)] peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-[#0b0a14] motion-reduce:transition-none">
<motion.span
aria-hidden
className="absolute inset-0 bg-[var(--tc)]"
initial={false}
animate={{ clipPath: on ? "circle(72px at 44px 50%)" : "circle(0px at 16px 50%)" }}
transition={reduce ? { duration: 0 } : { duration: 0.32, ease: [0.2, 0.8, 0.2, 1] }}
/>
<motion.span
aria-hidden
className="absolute top-1 left-1 h-6 rounded-full bg-white shadow-md shadow-black/30"
initial={false}
animate={{ x: on ? 24 - stretch : 0, width: 24 + stretch }}
transition={spring}
/>
</span>
</span>
</label>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/toggle-craft.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/toggle-craft-demo.tsx
"use client"; import { useState } from "react"; import { ToggleCraft } from "@/components/ui/toggle-craft"; export default function ToggleCraftExample() { const [sync, setSync] = useState(true); return ( <div className="grid h-full place-items-center bg-[#0b0a14] p-5"> <div className="grid w-full max-w-xs gap-5"> <ToggleCraft label="Auto sync" hint={sync ? "Saved to every device" : "This device only"} checked={sync} onChange={setSync} name="sync" /> <ToggleCraft label="Weekly digest" accent="#34d399" /> <ToggleCraft label="Beta features" hint="Not on your plan" disabled /> </div> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| label | string | — | Text next to the switch. |
| hint | string | — | Small text under the label. |
| checked | boolean | — | On or off (controlled). |
| defaultChecked | boolean | false | On or off at start (uncontrolled). |
| onChange | (checked: boolean) => void | — | Called with the new state. |
| accent | string | "#a78bfa" | Colour of the on state. |
| className | string | "w-full max-w-xs" | Wrapper look. |
| ...rest | input props | — | name, disabled, required and so on go to the input. |