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