Text Scroll Reveal
Each word lights up as the paragraph scrolls through the screen.
Scroll down ↓
Everywordlightsupasyouscroll,turningastaticparagraphintoamomentpeopleactuallyreadfromstarttofinish.
components/ui/text-scroll-reveal.tsx
"use client";
import { motion, useScroll, useTransform, type MotionValue } from "motion/react";
import { useRef, type RefObject } from "react";
function Word({ children, progress, range }: { children: string; progress: MotionValue<number>; range: [number, number] }) {
const opacity = useTransform(progress, range, [0.15, 1]);
const y = useTransform(progress, range, [6, 0]);
return (
<motion.span style={{ opacity, y }} className="mr-[0.25em] inline-block">
{children}
</motion.span>
);
}
type TextScrollRevealProps = {
text: string;
className?: string;
/** Track a scrollable element instead of the whole page */
container?: RefObject<HTMLElement | null>;
};
export function TextScrollReveal({ text, className = "", container }: TextScrollRevealProps) {
const ref = useRef<HTMLParagraphElement>(null);
const { scrollYProgress } = useScroll({ target: ref, container, offset: ["start 0.85", "end 0.45"] });
const words = text.split(" ");
return (
<p ref={ref} aria-label={text} className={`flex flex-wrap ${className}`}>
{words.map((word, i) => {
const start = i / words.length;
return (
<Word key={`${word}-${i}`} progress={scrollYProgress} range={[start, start + 1 / words.length]}>
{word}
</Word>
);
})}
</p>
);
}
export default function Demo() {
const ref = useRef<HTMLDivElement>(null);
return (
<div ref={ref} className="h-[380px] w-full max-w-2xl overflow-y-auto rounded-2xl border border-zinc-200 bg-white px-8">
<div className="flex h-56 items-end justify-center pb-6 text-sm text-zinc-400">Scroll down ↓</div>
<TextScrollReveal
container={ref}
text="Every word lights up as you scroll, turning a static paragraph into a moment people actually read from start to finish."
className="text-3xl font-semibold leading-tight tracking-tight text-zinc-900"
/>
<div className="h-72" />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/text-scroll-reveal.tsxcomponents/ui/text-scroll-reveal.tsx"use client"; import { motion, useScroll, useTransform, type MotionValue } from "motion/react"; import { useRef, type RefObject } from "react"; function Word({ children, progress, range }: { children: string; progress: MotionValue<number>; range: [number, number] }) { const opacity = useTransform(progress, range, [0.15, 1]); const y = useTransform(progress, range, [6, 0]); return ( <motion.span style={{ opacity, y }} className="mr-[0.25em] inline-block"> {children} </motion.span> ); } type TextScrollRevealProps = { text: string; className?: string; /** Track a scrollable element instead of the whole page */ container?: RefObject<HTMLElement | null>; }; export function TextScrollReveal({ text, className = "", container }: TextScrollRevealProps) { const ref = useRef<HTMLParagraphElement>(null); const { scrollYProgress } = useScroll({ target: ref, container, offset: ["start 0.85", "end 0.45"] }); const words = text.split(" "); return ( <p ref={ref} aria-label={text} className={`flex flex-wrap ${className}`}> {words.map((word, i) => { const start = i / words.length; return ( <Word key={`${word}-${i}`} progress={scrollYProgress} range={[start, start + 1 / words.length]}> {word} </Word> ); })} </p> ); } export default function Demo() { const ref = useRef<HTMLDivElement>(null); return ( <div ref={ref} className="h-[380px] w-full max-w-2xl overflow-y-auto rounded-2xl border border-zinc-200 bg-white px-8"> <div className="flex h-56 items-end justify-center pb-6 text-sm text-zinc-400">Scroll down ↓</div> <TextScrollReveal container={ref} text="Every word lights up as you scroll, turning a static paragraph into a moment people actually read from start to finish." className="text-3xl font-semibold leading-tight tracking-tight text-zinc-900" /> <div className="h-72" /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.