Scroll Text Highlight
Reading highlight that turns text from pale grey to solid foreground, character by character (or word by word), as you scroll.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add scroll-text-highlight.enScroll Text Highlight is a React scroll-reveal text component built on Motion's useScroll: as a paragraph scrolls through the viewport, its letters or words fill in from faint gray to solid color, one after another. It also works inside nested scroll containers.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ScrollTextHighlight/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- A mission statement or "about us" paragraph that readers take in at scroll pace
- Transition copy between two large visuals on a product launch page
- Pull quotes in long-form scrollytelling pages
- Slide-style single-page intros with one sentence per screen
Installation
npx webberui@latest add https://webberui.com/r/scroll-text-highlight.en.jsonOr, once registries are configured in components.json, install it as @webberui/scroll-text-highlight.
Usage
Inside a nested scroll container, pass the container's ref to container:
import * as React from "react";
import { ScrollTextHighlight } from "@/components/ui/scroll-text-highlight";
export function Example() {
const scrollerRef = React.useRef<HTMLDivElement>(null);
return (
<div ref={scrollerRef} className="h-[300px] overflow-y-auto">
<div className="py-40">
<ScrollTextHighlight
container={scrollerRef}
text="Text that lights up character by character as you scroll."
className="text-xl font-semibold"
/>
</div>
</div>
);
}When the window itself is the scroll container, container can be omitted:
<ScrollTextHighlight
by="word"
text="Scroll to reveal."
className="text-4xl font-bold"
/>Examples
Brand manifesto section
Each ScrollTextHighlight tracks its own scroll progress, so several paragraphs light up in sequence. Vertical padding gives each paragraph enough scroll distance.
import { ScrollTextHighlight } from "@/components/ui/scroll-text-highlight";
const paragraphs = [
"We think good tools should stay quietly in the background.",
"That's why every component ships with its full source: read it, change it, or take all of it.",
];
export function Manifesto() {
return (
<section className="mx-auto max-w-3xl space-y-24 py-[40vh]">
{paragraphs.map((p) => (
<ScrollTextHighlight
key={p}
text={p}
className="text-3xl font-semibold leading-relaxed md:text-5xl"
/>
))}
</section>
);
}Set the text color
The unlit part is the same color at 0.2 opacity, so setting the text color with className is enough, with separate values for light and dark mode.
<ScrollTextHighlight
text="Deploy in three minutes, no servers to configure."
className="text-4xl font-bold text-indigo-600 dark:text-indigo-300"
/>Word-by-word for English copy
by="word" splits on spaces, which suits space-separated languages like English and gives a slower rhythm than per-letter.
<ScrollTextHighlight
by="word"
text="Ship faster with components you fully own."
className="text-5xl font-semibold tracking-tight"
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | — | The text to highlight as you scroll |
by | "char" | "word" | "char" | Smallest unit of highlighting |
container | RefObject<HTMLElement> | — | Ref of the nested scroll container; omit to use the window as the scroll container |
className | string | — | Extra styles (set font size and text color here) |
How it works
useScroll({ target, container, offset: ["start end", "end start"] })yieldsscrollYProgress, the scroll progress of the target text from entering the bottom of the container to leaving the top- Each character (or word) is assigned a
[start, end]sub-range by index, anduseTransformmaps progress ontoopacity, turning them one by one from pale grey to solid foreground - Character mode splits on graphemes, so emoji and combining characters are never broken apart
- The text inherits
currentColor(the foreground color), which naturally reads as light grey while faded; set font size and text color viaclassName
Accessibility
- The complete text is rendered
sr-onlyfor assistive technology, and the per-character animation layer is markedaria-hidden, so nothing is announced twice - When the user has "reduce motion" enabled at the system level, the text renders statically in full foreground color
FAQ
Can I use by="word" for Chinese or Japanese?
Not really. Word mode splits on spaces, and CJK sentences have none, so the whole sentence becomes one unit that lights up at once. Keep the default by="char" for those languages.
Does it work with the Next.js App Router?
Yes. The source already has "use client", so import it from a Server Component page. The reduced-motion static version only switches in after mount, so the first render matches the server output.
When does the highlight start and finish?
The scroll range is offset: ["start end", "end start"]: it starts when the top of the paragraph reaches the bottom of the viewport and finishes when its bottom leaves the top, with each unit getting an equal slice, so the last characters light up as the paragraph nears the top of the screen.
Can text contain bold text or links?
No. text is typed as string; the component splits it into letters or words and sets opacity on each. For mixed styling, split it into several components or style the wrapper.
Will screen readers read it twice?
No. The full text sits in an sr-only span and the animated layer is aria-hidden. The only runtime import is motion.