Baktiyar Ablaikhan

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).