Retour au catalogue

About Scroll Reveal

Le texte de la mission se revele mot par mot au scroll. Chaque mot passe de opacity 0.15 a 1 progressivement. Effet Dior.com. useScroll + useTransform.

aboutcomplex Both Responsive a11y
eleganteditorialelegantagencyportfoliouniversalstacked
Theme

How to build a scroll-driven word reveal in React with Framer Motion

To reveal text word by word on scroll in React, split the string into words, map each to a motion.span, and drive its opacity with useTransform against a useScroll progress value. Each word gets a proportional [start, end] range so the reveal flows left to right as the user scrolls.

  • Stack: React 18 + Framer Motion 11, ~135 lines, zero extra dependencies beyond framer-motion.
  • Core API: useScroll (target + offset), useTransform, motion.span with willChange:'opacity'.
  • The section requires at least 80vh of scroll space to play through; best with 20-40 word paragraphs.
  • Accessible: the full paragraph text is present in the DOM at all times; opacity is purely visual.
  • Responsive by design, font size uses clamp() and the layout reflows naturally on mobile.

About Scroll Reveal is a React section that animates a mission or manifesto paragraph word by word as the user scrolls through it. Each word fades from near-invisible to full opacity in sequence, turning a block of text into a statement that earns its read. The pattern is borrowed from high-end brand sites like Dior.com and now accessible in a single copy-paste component.

Anatomy

The section is structured in three vertical blocks inside a centered container: a small pill badge with an uppercase label, an h2 title that slides up once on enter, and a large paragraph where every word is wrapped in its own motion.span. The paragraph uses display:flex + flex-wrap to keep word spans inline and line-breaking naturally. The container ref is attached to the word paragraph wrapper so the scroll offset tracks that specific DOM node.

How it works

The animation is driven entirely by Framer Motion's useScroll hook, which returns a scrollYProgress value from 0 to 1 as the target element travels through the viewport. The offset `['start 0.8', 'end 0.3']` means progress starts when the top of the section hits 80% down the screen and ends when the bottom reaches 30%. Words are split with text.split(' '), and each word receives a proportional range: word i gets [i / totalWords, (i+1) / totalWords]. useTransform then maps that range to an opacity of [0.15, 1], so words light up in sequence rather than all at once.

How to build it in React

  1. Set up the scroll context with a container ref

    Create a ref and pass it as the target to useScroll. Set the offset so the animation spans a comfortable window as the section scrolls into view. The offset values are in the format [triggerPoint, endPoint] where '0.8' means 80% down the viewport.

    const containerRef = useRef<HTMLDivElement>(null);
    const { scrollYProgress } = useScroll({
      target: containerRef,
      offset: ["start 0.8", "end 0.3"],
    });
  2. Split the paragraph into words and assign ranges

    Split the text string by spaces to get an array of words. For each word at index i in an array of totalWords elements, compute start = i / totalWords and end = (i + 1) / totalWords. These two numbers become the input range for useTransform.

    const words = text.split(" ");
    const totalWords = words.length;
    // inside the map:
    const start = i / totalWords;
    const end = (i + 1) / totalWords;
  3. Animate each word's opacity with useTransform

    In a RevealWord sub-component, call useTransform to map the [start, end] range from scrollYProgress to an opacity of [0.15, 1]. Apply the resulting motion value to a motion.span. Add willChange:'opacity' to hint the browser for GPU compositing.

    function RevealWord({ word, range, progress }) {
      const opacity = useTransform(progress, range, [0.15, 1]);
      return (
        <motion.span style={{ opacity, willChange: "opacity" }}>
          {word}
        </motion.span>
      );
    }
  4. Wrap words in a flex container

    Render the words inside a p tag styled with display:flex and flex-wrap:wrap. Each motion.span needs a marginRight to add word spacing, since inline-block spans don't inherit normal whitespace. Set a large font size with clamp() so the text reads as a statement, not body copy.

    <p style={{ display: "flex", flexWrap: "wrap", fontSize: "clamp(1.5rem, 3.5vw, 2.75rem)" }}>
      {words.map((word, i) => (
        <RevealWord key={i} word={word} range={[i / total, (i+1) / total]} progress={scrollYProgress} />
      ))}
    </p>

When to use it

Reach for this pattern when you have a 20-40 word mission statement or manifesto that deserves more than a static block of text. It works well on agency, portfolio, and brand landing pages placed early to mid-page. Skip it on data-dense dashboards, e-commerce product pages, or any context where users are scanning for a specific answer rather than reading. The section also needs enough vertical height for the scroll animation to breathe; on very short pages the effect fires too quickly.

Used by

  • Dior, Pioneered word-by-word scroll reveals on brand storytelling pages as a signature editorial interaction.
  • Stripe, Uses scroll-driven text animations on campaign and about pages to pace the reading of product narratives.
  • Lusion, Portfolio studio site using progressive text reveals as a primary storytelling device throughout the page.
  • Awwwards, The pattern consistently appears in award-winning agency sites catalogued on the platform, making it a recognized premium UI gesture.

FAQ

Why does the animation feel too fast or too slow?

The speed is controlled by the offset option in useScroll. Tighten the range (e.g. `['start 0.6', 'end 0.5']`) to speed it up, or widen it (e.g. `['start 0.9', 'end 0.1']`) to slow it down. You can also set minHeight on the section to force a taller scroll area.

Can I start words at full transparency (opacity 0) instead of 0.15?

Technically yes, change the output range in useTransform to [0, 1]. In practice, starting at 0.15 keeps the text structure visible so users perceive there is content to reveal. Full zero makes the paragraph look empty before scrolling, which can feel broken.

Does this work on mobile?

The scroll animation works on touch devices, there is no pointer dependency. The section is marked responsive:true and uses clamp() for font sizing. The main consideration is that the minHeight of 80vh takes up most of the screen; on very short content pages this can feel heavy.

How do I handle punctuation gaps in the word split?

text.split(' ') is sufficient for standard prose. If your text has multiple spaces or special whitespace, filter out empty strings with .filter(Boolean) after the split. Punctuation attached to a word (like a comma) stays with that word and animates together, which is the desired behavior.

"use client";

import { useRef } from "react";
import { motion, useScroll, useTransform } from "framer-motion";

interface AboutScrollRevealProps {
  label?: string;
  title?: string;
  text?: string;
}

function RevealWord({
  word,
  range,
  progress,
}: {
  word: string;
  range: [number, number];
  progress: ReturnType<typeof useScroll>["scrollYProgress"];
}) {
  const opacity = useTransform(progress, range, [0.15, 1]);
  return (

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reviews

React Scroll-Reveal Text Animation, Word by Word