Range
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Drag along the ticks to set a value; ticks swell under your pointer.
Code
"use client";
import { animate, motion, useMotionValue, useMotionValueEvent } from "motion/react";
import { useRef, useState } from "react";
import { Surface } from "../ui";
const TICKS = 41;
export default function Range() {
const bar = useRef<HTMLDivElement>(null);
const value = useMotionValue(50);
const [v, setV] = useState(50);
const [hover, setHover] = useState<number | null>(null);
const [active, setActive] = useState(false);
useMotionValueEvent(value, "change", setV);
const at = (clientX: number) => {
const r = bar.current!.getBoundingClientRect();
return Math.min(1, Math.max(0, (clientX - r.left) / r.width));
};
const shown = Math.round(v);
return (
<Surface>
<div className="flex items-center gap-[1.2em]">
<div
ref={bar}
className="relative flex h-[3.6em] w-[17em] cursor-pointer touch-none items-center justify-between"
onPointerDown={(e) => {
e.currentTarget.setPointerCapture(e.pointerId);
setActive(true);
const t = at(e.clientX);
animate(value, t * 100, { type: "spring", stiffness: 400, damping: 35 });
setHover(t);
}}
onPointerMove={(e) => {
const t = at(e.clientX);
setHover(t);
if (active) value.set(t * 100);
}}
onPointerUp={() => setActive(false)}
onPointerCancel={() => setActive(false)}
onPointerLeave={() => !active && setHover(null)}
>
{Array.from({ length: TICKS }, (_, i) => {
const pos = i / (TICKS - 1);
const on = pos * 100 <= v + 0.001;
const near = hover === null ? 0 : Math.max(0, 1 - Math.abs(pos - hover) * 9);
return (
<motion.span
key={i}
className="block w-[0.16em] rounded-full"
animate={{
height: `${1.5 + near * 1.3 + (i % 5 === 0 ? 0.3 : 0)}em`,
backgroundColor: on ? "#151515" : "#c9c8c3",
}}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
);
})}
</div>
<div className="relative h-[1.6em] w-[2.2em] overflow-hidden font-mono text-[1.05em] tabular-nums">
<motion.span key={shown} initial={{ y: "-60%", opacity: 0 }} animate={{ y: "0%", opacity: 1 }} className="absolute left-0">
{shown}
</motion.span>
</div>
</div>
</Surface>
);
}
Prompt
Build "Range", a input animation. What it does: Drag along the ticks to set a value; ticks swell under your pointer. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using motion. Technique: an HTML canvas, the Motion library, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).