A paragraph that reads itself in as you scroll: words start faint and brighten one after another, at the pace of your scrolling.
Scroll
Great interfaces don't shout. They answer every click, guide every scroll and get out of the way, so the work itself can shine.
Customize
Usage
import { ScrollFillText } from "@/components/motionkit/scroll-fill-text";
<ScrollFillText
text="Great interfaces don't shout. They answer every click, guide every scroll and get out of the way, so the work itself can shine."
className="max-w-3xl text-4xl font-medium leading-tight"
/>Motion
The exact curves this component runs, plotted with Motion's own maths.
Word fill
Scroll position
Mission statements, manifesto sections and big pull quotes on product and agency sites: one short paragraph that deserves to be read slowly.
useScroll tracks the paragraph from when its top rises past 85% of the screen to when its end passes 45%. That progress is shared out between the words: each word owns an equal slice, and useTransform maps its slice to an opacity from the dim level up to full. Words brighten in reading order as you scroll and fade back if you scroll up. Each word is a span inside the real paragraph, so the text stays selectable and is read normally.
Works in the App Router as is. Requires Tailwind CSS v4.
One command adds the file and installs its dependencies. Needs a components.json (run npx shadcn@latest init once if you don't have one).
npx shadcn@latest add https://www.motionkitui.com/r/scroll-fill-text.jsonnpm install motionSave as components/motionkit/scroll-fill-text.tsx.
"use client";
import { useRef, type ElementType, type RefObject } from "react";
import { motion, useReducedMotion, useScroll, useTransform, type MotionValue } from "motion/react";
type ScrollFillTextProps = {
text: string;
/** How faint words are before you reach them, 0 to 1. */
dim?: number;
/** The element to render, e.g. "p" or "h2". */
as?: ElementType;
/** A scrolling element to track instead of the page, e.g. a panel with overflow: auto. */
container?: RefObject<HTMLElement | null>;
className?: string;
};
function Word({ word, progress, range, dim }: { word: string; progress: MotionValue<number>; range: [number, number]; dim: number }) {
const opacity = useTransform(progress, range, [dim, 1]);
return <motion.span style={{ opacity }}>{word}</motion.span>;
}
/**
* A paragraph that reads itself in as you scroll: words start faint and brighten one after another,
* so the text fills at the pace of the reader's scrolling. The words are always real, selectable text.
*/
export function ScrollFillText({ text, dim = 0.18, as: Tag = "p", container, className = "" }: ScrollFillTextProps) {
const reduceMotion = useReducedMotion();
const ref = useRef<HTMLElement>(null);
// starts as the paragraph rises into the lower part of the screen, finishes before it reaches the top
const { scrollYProgress } = useScroll({ target: ref, container, offset: ["start 0.85", "end 0.45"] });
const words = text.split(/(\s+)/);
const count = words.filter((word) => word.trim()).length;
let index = 0;
return (
<Tag ref={ref} className={className}>
{reduceMotion
? text
: words.map((word, key) => {
if (!word.trim()) return word;
const start = index / count;
index += 1;
return <Word key={key} word={word} progress={scrollYProgress} range={[start, start + 1 / count]} dim={dim} />;
})}
</Tag>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | — | The paragraph. |
| dim | number | 0.18 | Opacity of words not yet reached. |
| as | ElementType | "p" | The element to render. |
| container | RefObject<HTMLElement> | — | A scrolling element to follow instead of the page. |
How we audit: the WCAG 2.2 animation checklist and respecting reduced motion in React.
Yes. The file starts with "use client", so you can import it straight into a Server Component page or layout. It works in Vite as is, and in Astro as a React island.
Only the motion package, beside React and Tailwind CSS v4. There is no MotionKit package and no Tailwind config to add.
Yes. Run npx shadcn@latest add https://www.motionkitui.com/r/scroll-fill-text.json in any project with a components.json. It saves the file to components/motionkit and installs its dependencies.
With reduced motion on, the paragraph shows at full strength with no fill.
Yes. Pass a ref to the scrolling element as container, and the fill follows that element's scroll position instead of the window's.
Kinetic Text
Headlines that rise letter by letter, slide up line by line, blur into focus or light up as you scroll.
Scroll Word Swap
A pinned sentence whose key word changes as you scroll, with the space around it resizing smoothly.
Word Rise
Words lift out of a blur one after another as the line scrolls into view.