Baktiyar Ablaikhan

Wake slider

Source
After bencho.dev ↗
Author
Interaction after bencho.dev
Licence
Original code — interaction after bencho.dev

Loading…

Drag the slider quickly and the bars ripple in its wake.

Code

"use client";

import { useEffect, useRef, useState } from "react";
import { prefersReducedMotion, Surface } from "../ui";

const BARS = 36;

export default function WakeSlider() {
  const track = useRef<HTMLDivElement>(null);
  const bars = useRef<(HTMLSpanElement | null)[]>([]);
  const thumb = useRef<HTMLSpanElement>(null);
  const sim = useRef({ t: 0.4, target: 0.4, drag: false, h: new Float32Array(BARS), v: new Float32Array(BARS) });
  const [value, setValue] = useState(40);

  useEffect(() => {
    const still = prefersReducedMotion();
    let raf = 0;
    let shown = -1;
    const loop = () => {
      const s = sim.current;
      const prev = s.t;
      s.t += (s.target - s.t) * 0.25;
      const speed = (s.t - prev) * BARS;
      const at = s.t * (BARS - 1);
      // the moving thumb pushes the water around it
      for (let i = 0; i < BARS; i++) {
        const d = i - at;
        if (Math.abs(d) < 2.5) s.v[i] += Math.abs(speed) * 0.9 * (1 - Math.abs(d) / 2.5);
      }
      // a simple 1D wave: neighbours pull on each other, everything settles
      const next = new Float32Array(BARS);
      for (let i = 0; i < BARS; i++) {
        const l = s.h[Math.max(0, i - 1)];
        const r = s.h[Math.min(BARS - 1, i + 1)];
        s.v[i] += (l + r - 2 * s.h[i]) * 0.25 - s.h[i] * 0.04;
        s.v[i] *= 0.94;
        next[i] = s.h[i] + s.v[i];
      }
      s.h = next;
      for (let i = 0; i < BARS; i++) {
        const el = bars.current[i];
        if (!el) continue;
        const on = i <= at;
        el.style.transform = `scaleY(${1 + Math.max(-0.5, Math.min(2.4, s.h[i] * 2.2))})`;
        el.style.backgroundColor = on ? "#151515" : "#cfcec8";
      }
      if (thumb.current) thumb.current.style.left = `${s.t * 100}%`;
      const v = Math.round(s.t * 100);
      if (v !== shown) {
        shown = v;
        setValue(v);
      }
      if (!still) raf = requestAnimationFrame(loop);
    };
    loop();
    return () => cancelAnimationFrame(raf);
  }, []);

  const set = (x: number) => {
    const r = track.current!.getBoundingClientRect();
    sim.current.target = Math.min(1, Math.max(0, (x - r.left) / r.width));
  };

  return (
    <Surface>
      <div className="flex w-[22em] flex-col gap-[1.2em]">
        <div className="flex items-baseline justify-between">
          <span className="text-[0.8em] text-[#8a8984]">Volume</span>
          <span className="font-mono text-[1.4em] tabular-nums">{value}</span>
        </div>
        <div
          ref={track}
          className="relative flex h-[4.4em] cursor-pointer touch-none items-center justify-between"
          onPointerDown={(e) => {
            e.currentTarget.setPointerCapture(e.pointerId);
            sim.current.drag = true;
            set(e.clientX);
          }}
          onPointerMove={(e) => sim.current.drag && set(e.clientX)}
          onPointerUp={() => (sim.current.drag = false)}
          onPointerCancel={() => (sim.current.drag = false)}
        >
          {Array.from({ length: BARS }, (_, i) => (
            <span key={i} ref={(n) => void (bars.current[i] = n)} className="block h-[1.2em] w-[0.22em] rounded-full bg-[#cfcec8] will-change-transform" />
          ))}
          <span ref={thumb} className="pointer-events-none absolute top-1/2 h-[2.4em] w-[0.3em] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[#2B46B8] shadow-[0_0_0_0.25em_rgba(43,70,184,0.15)]" />
        </div>
        <p className="text-center font-mono text-[0.7em] text-[#8a8984]">drag fast to make a wake</p>
      </div>
    </Surface>
  );
}
Prompt

Build "Wake slider", a input animation. What it does: Drag the slider quickly and the bars ripple in its wake. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS. Technique: an HTML canvas, a requestAnimationFrame loop, 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).