Customize
Usage
import { LiquidToggle } from "@/components/motionkit/liquid-toggle";
const [enabled, setEnabled] = useState(true);
<LiquidToggle
label="Email notifications"
checked={enabled}
onChange={setEnabled}
/>Motion
The exact curves this component runs, plotted with Motion's own maths.
Knob travel
Switch flips
Works in the App Router as is. Requires Tailwind CSS v4.
npm install motionSave as components/motionkit/liquid-toggle.tsx.
"use client";
import { useId, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
type LiquidToggleProps = {
/** Visible label, also used as the accessible name. */
label: string;
/** Controlled state. Leave unset to let the toggle manage itself. */
checked?: boolean;
defaultChecked?: boolean;
onChange?: (checked: boolean) => void;
/** Track height in pixels; the width follows. */
size?: number;
/** Track colour when on. */
color?: string;
className?: string;
};
export function LiquidToggle({
label,
checked,
defaultChecked = false,
onChange,
size = 28,
color = "#0E7C86",
className = "",
}: LiquidToggleProps) {
const [internal, setInternal] = useState(defaultChecked);
const on = checked ?? internal;
const reduceMotion = useReducedMotion();
const id = useId();
const pad = 3;
const knob = size - pad * 2;
const travel = size * 0.85;
function toggle() {
setInternal(!on);
onChange?.(!on);
}
return (
<label htmlFor={id} className={`inline-flex cursor-pointer select-none items-center gap-3 text-sm text-[#DCE3E2] ${className}`}>
<button
id={id}
type="button"
role="switch"
aria-checked={on}
onClick={toggle}
className="relative shrink-0 rounded-full border border-white/10 transition-colors duration-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#5FAEB4]"
style={{ width: size + travel, height: size, backgroundColor: on ? color : "#1D2727" }}
>
{/* the knob stretches toward where it is going, then settles: the "liquid" part */}
<motion.span
aria-hidden
className="absolute rounded-full bg-[#F6F1E8] shadow-md shadow-black/30"
style={{ top: pad - 1, left: pad - 1, height: knob, width: knob, originX: on ? 1 : 0 }}
initial={false}
animate={{ x: on ? travel : 0, scaleX: reduceMotion ? 1 : [1, 1.45, 1] }}
transition={
reduceMotion
? { duration: 0 }
: { x: { type: "spring", bounce: 0.3, duration: 0.45 }, scaleX: { duration: 0.4, times: [0, 0.4, 1] } }
}
/>
</button>
{label}
</label>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | — | Visible label, also the accessible name. |
| checked / onChange | boolean / (checked) => void | — | Use both to control it yourself. |
| defaultChecked | boolean | false | Starting state when uncontrolled. |
| size | number | 28 | Track height in pixels; the width follows. |
| color | string | "#0E7C86" | Track colour when on. |
Code Input
One-time-code boxes where each digit pops in, with paste and phone autofill built in.
Magnetic Button
A button that leans toward the cursor with a springy, physical pull.
Pull me
Magnetic Letters
Each letter leans toward the cursor and springs home, like a magnetic button for every character.