Baktiyar Ablaikhan

Text fade

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

Loading…

A sentence fades in letter by letter; slide across to scrub it.

Code

"use client";

import { animate, useMotionValue, useMotionValueEvent } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { Surface } from "../ui";

const TEXT = "Words arrive the way light does — a little at a time, then all at once.";
const CHARS = TEXT.split("");
const TAIL = 9;

export default function TextFade() {
  const progress = useMotionValue(0);
  const [p, setP] = useState(0);
  const hovering = useRef(false);
  const box = useRef<HTMLDivElement>(null);
  useMotionValueEvent(progress, "change", setP);

  const play = () => {
    progress.jump(0);
    animate(progress, 1, { duration: 2.6, ease: "linear" });
  };

  useEffect(() => {
    const c = animate(progress, 1, { duration: 2.6, ease: "linear", delay: 0.3 });
    return () => c.stop();
  }, [progress]);

  const head = p * (CHARS.length + TAIL);

  return (
    <Surface>
      <div
        ref={box}
        className="w-[80%] max-w-[24em] cursor-ew-resize touch-none py-[2em]"
        onPointerMove={(e) => {
          if (e.pointerType === "touch" && e.buttons === 0) return;
          hovering.current = true;
          const r = box.current!.getBoundingClientRect();
          const t = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width));
          animate(progress, t, { type: "spring", stiffness: 200, damping: 30 });
        }}
        onPointerLeave={() => {
          if (!hovering.current) return;
          hovering.current = false;
          animate(progress, 1, { duration: (1 - progress.get()) * 1.6, ease: "linear" });
        }}
        onClick={play}
      >
        <p className="font-serif text-[2.1em] leading-[1.15] text-[#151515]">
          {CHARS.map((ch, i) => {
            const o = Math.min(1, Math.max(0, (head - i) / TAIL));
            return (
              <span key={i} style={{ opacity: o, filter: o < 1 ? `blur(${(1 - o) * 3}px)` : undefined }}>
                {ch}
              </span>
            );
          })}
        </p>
        <div className="mt-[1.2em] h-[2px] w-full rounded-full bg-[#e4e3de]">
          <div className="h-full rounded-full bg-[#151515]" style={{ width: `${p * 100}%` }} />
        </div>
      </div>
      <p className="pointer-events-none absolute bottom-[6%] font-mono text-[0.7em] text-[#8a8984]">slide across to scrub · click to replay</p>
    </Surface>
  );
}
Prompt

Build "Text fade", a text animation. What it does: A sentence fades in letter by letter; slide across to scrub it. 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).