Text Animations

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

New project or installation trouble? Start here →

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

Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
textstring—The text to highlight as you scroll
by"char" | "word""char"Smallest unit of highlighting
containerRefObject<HTMLElement>—Ref of the nested scroll container; omit to use the window as the scroll container
classNamestring—Extra styles (set font size and text color here)

How it works

  • useScroll({ target, container, offset: ["start end", "end start"] }) yields scrollYProgress, 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, and useTransform maps progress onto opacity, 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 via className

Accessibility

  • The complete text is rendered sr-only for assistive technology, and the per-character animation layer is marked aria-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.

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.