# Scroll Fill Text

> A paragraph that reads itself in as you scroll: words start faint and brighten one after another, at the pace of your scrolling.

Category: Text · Free · Updated 2026-10-26 · Live demo: https://www.motionkitui.com/components/scroll-fill-text

## When to use it

Mission statements, manifesto sections and big pull quotes on product and agency sites: one short paragraph that deserves to be read slowly.

## How it works

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.

## Installation

```bash
npx shadcn@latest add https://www.motionkitui.com/r/scroll-fill-text.json
```

Or copy `components/motionkit/scroll-fill-text.tsx` below. Requires react, motion and Tailwind CSS v4. Free for personal projects; commercial use needs MotionKit Plus (https://www.motionkitui.com/pricing).

## Props

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

## Accessibility

- **Keyboard: Not applicable.** Text only, nothing to focus. Keyboard scrolling drives it like any other.
- **Screen readers: Pass.** The words are ordinary text in reading order, always readable whatever their brightness.
- **Reduced motion: Pass.** With reduced motion on, the paragraph shows at full strength with no fill.
- **Colour contrast: Partial.** Words reach 17:1 as they fill. Words not yet reached are deliberately faint, 1.6:1 at the default dim of 0.18, so this suits short statements rather than text people must read before scrolling.

## Source

```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>
  );
}
```
