Text Effects
ScrollTypography
Words light up one by one as the paragraph scrolls up the screen, so the reading pace follows the scroll.
Download
- React
- Tailwind CSS v4
- motion
components/ui/scroll-typography.tsx
"use client";
import { useRef, type ReactNode, type RefObject } from "react";
import { motion, useReducedMotion, useScroll, useTransform, type MotionValue } from "motion/react";
type ScrollTypographyProps = {
text: string;
className?: string;
/** Opacity of words not reached yet, 0 to 1. */
dim?: number;
/** Scrolling box to follow. Leave out to follow the page. */
container?: RefObject<HTMLElement | null>;
};
function Word({ children, progress, range, dim }: { children: ReactNode; progress: MotionValue<number>; range: [number, number]; dim: number }) {
const opacity = useTransform(progress, range, [dim, 1]);
return <motion.span style={{ opacity }}>{children}</motion.span>;
}
/* ScrollTypography: words light up one by one as the paragraph scrolls up the screen, so the reading pace follows the scroll. */
export function ScrollTypography({ text, className = "", dim = 0.15, container }: ScrollTypographyProps) {
const reduce = useReducedMotion();
const ref = useRef<HTMLParagraphElement>(null);
const { scrollYProgress } = useScroll({ target: ref, container, offset: ["start 0.85", "end 0.4"] });
const words = text.split(" ");
return (
<p ref={ref} className={className}>
<span className="sr-only">{text}</span>
<span aria-hidden>
{words.map((word, i) => (
<span key={i}>
<Word progress={scrollYProgress} range={[i / words.length, (i + 1) / words.length]} dim={reduce ? 1 : dim}>
{word}
</Word>
{i < words.length - 1 && " "}
</span>
))}
</span>
</p>
);
}
1. Install the dependency.
npm i motion2. Paste the Code tab into
components/ui/scroll-typography.tsx. The styles are Tailwind CSS v4 classes.3. Use it. This is the preview above:
app/scroll-typography-demo.tsx
"use client"; import { useRef } from "react"; import { ScrollTypography } from "@/components/ui/scroll-typography"; export default function ScrollTypographyExample() { // The preview scrolls inside its own box. On a real page, leave out `container`. const box = useRef<HTMLDivElement>(null); return ( <div ref={box} className="relative h-full overflow-y-auto bg-[#121212] px-6"> <p className="sticky top-0 z-10 bg-[#121212] py-3 text-center text-xs text-[#f4f1ea]/50">Scroll inside this box</p> <div className="h-[70%]" /> <ScrollTypography container={box} text="Good writing is read at the pace it was meant to be read. Let the scroll set the rhythm, one word at a time." className="mx-auto max-w-xl text-[clamp(22px,3.5vw,36px)] font-semibold leading-snug text-[#f4f1ea]" /> <div className="h-[80%]" /> </div> ); }
Props
| Name | Type | Default | What it does |
|---|---|---|---|
| text | string | — | Paragraph text. |
| dim | number | 0.15 | Opacity of words not reached yet, 0 to 1. |
| container | RefObject<HTMLElement> | — | Scrolling box to follow. Leave out to follow the page. |
| className | string | "" | Type styles for the text. |