Cómo crear una revelación de palabras al scroll en React con Framer Motion
Para revelar un texto palabra a palabra al hacer scroll en React, divide la cadena en palabras, mapea cada una a un motion.span y controla su opacidad con useTransform sobre el valor de progreso de useScroll. Cada palabra recibe un intervalo [start, end] proporcional para que la revelación fluya de izquierda a derecha a medida que el usuario hace scroll.
- Stack: React 18 + Framer Motion 11, ~135 líneas, cero dependencias adicionales más allá de framer-motion.
- API principal: useScroll (target + offset), useTransform, motion.span con willChange:'opacity'.
- La sección necesita al menos 80vh de espacio de scroll para desplegarse; ideal con párrafos de 20 a 40 palabras.
- Accesible: el texto completo del párrafo está siempre presente en el DOM; la opacidad es puramente visual.
- Responsive por diseño, el tamaño de fuente usa clamp() y el layout se reajusta de forma natural en móvil.
About Scroll Reveal es una sección React que anima un párrafo de misión o manifiesto palabra a palabra a medida que el usuario hace scroll. Cada palabra pasa de una opacidad casi nula a la opacidad total en orden, transformando un bloque de texto en una declaración que se gana su lectura. El patrón está inspirado en sitios de marcas de alta gama como Dior.com, ahora accesible en un único componente copy-paste.
Anatomía
La sección se organiza en tres bloques verticales dentro de un contenedor centrado: un pequeño badge pill con una etiqueta en mayúsculas, un h2 que sube una vez al entrar, y un párrafo grande donde cada palabra se envuelve en su propio motion.span. El párrafo usa display:flex + flex-wrap para mantener los spans de palabras inline con saltos de línea naturales. La ref del contenedor se asocia al wrapper del párrafo de palabras para que el offset de scroll siga ese nodo DOM concreto.
Cómo funciona
La animación está enteramente controlada por el hook useScroll de Framer Motion, que devuelve un valor scrollYProgress de 0 a 1 mientras el elemento objetivo atraviesa el viewport. El offset `['start 0.8', 'end 0.3']` significa que el progreso empieza cuando la parte superior de la sección llega al 80% hacia abajo de la pantalla y termina cuando la parte inferior alcanza el 30%. Las palabras se dividen con text.split(' '), y cada una recibe un intervalo proporcional: la palabra i obtiene [i / totalWords, (i+1) / totalWords]. useTransform mapea entonces ese intervalo a una opacidad de [0.15, 1], de modo que las palabras se iluminan en secuencia en lugar de todas a la vez.
Cómo crearlo en React
Configurar el contexto de scroll con una ref de contenedor
Crea una ref y pásala como target a useScroll. Ajusta el offset para que la animación abarque una ventana cómoda mientras la sección entra en vista. Los valores del offset siguen el formato [puntoDeInicio, puntoDeFin] donde '0.8' significa 80% hacia abajo del viewport.
const containerRef = useRef<HTMLDivElement>(null); const { scrollYProgress } = useScroll({ target: containerRef, offset: ["start 0.8", "end 0.3"], });Dividir el párrafo en palabras y asignar los intervalos
Divide la cadena de texto por los espacios para obtener un array de palabras. Para cada palabra en el índice i dentro de un array de totalWords elementos, calcula start = i / totalWords y end = (i + 1) / totalWords. Estos dos números se convierten en el intervalo de entrada para useTransform.
const words = text.split(" "); const totalWords = words.length; // inside the map: const start = i / totalWords; const end = (i + 1) / totalWords;Animar la opacidad de cada palabra con useTransform
En un subcomponente RevealWord, llama a useTransform para mapear el intervalo [start, end] de scrollYProgress a una opacidad de [0.15, 1]. Aplica el valor motion resultante a un motion.span. Añade willChange:'opacity' para indicar al navegador la composición por GPU.
function RevealWord({ word, range, progress }) { const opacity = useTransform(progress, range, [0.15, 1]); return ( <motion.span style={{ opacity, willChange: "opacity" }}> {word} </motion.span> ); }Envolver las palabras en un contenedor flex
Renderiza las palabras dentro de una etiqueta p con display:flex y flex-wrap:wrap. Cada motion.span necesita un marginRight para añadir espaciado entre palabras, ya que los spans inline-block no heredan los espacios normales. Define un tamaño de fuente grande con clamp() para que el texto se lea como una declaración, no como cuerpo de texto.
<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>
Cuándo usarlo
Este patrón es pertinente cuando tienes una declaración de misión o un manifiesto de 20 a 40 palabras que merece más que un simple bloque de texto estático. Funciona bien en páginas de agencia, portfolio y landing de marca, colocado en la parte alta o media de la página. Evítalo en dashboards densos, páginas de producto de e-commerce o cualquier contexto donde los usuarios buscan una respuesta concreta en lugar de leer. La sección también necesita suficiente altura vertical para que la animación de scroll tenga espacio; en páginas muy cortas el efecto se dispara demasiado rápido.
Usado por
- Dior, Fue pionera en la revelación palabra a palabra en sus páginas de storytelling de marca como interacción editorial distintiva.
- Stripe, Usa animaciones de texto guiadas por el scroll en sus páginas de campaña y about para marcar el ritmo de lectura de los relatos de producto.
- Lusion, Sitio de estudio portfolio que usa la revelación progresiva de texto como recurso narrativo principal a lo largo de toda la página.
- Awwwards, Este patrón aparece de forma constante en los sitios de agencia premiados catalogados en la plataforma, lo que lo convierte en un gesto de UI premium reconocido.
FAQ
¿Por qué la animación parece demasiado rápida o demasiado lenta?
La velocidad se controla con la opción offset de useScroll. Estrecha el intervalo (ej. `['start 0.6', 'end 0.5']`) para acelerarla, o amplíalo (ej. `['start 0.9', 'end 0.1']`) para ralentizarla. También puedes fijar un minHeight en la sección para forzar una zona de scroll más alta.
¿Puedo iniciar las palabras con transparencia total (opacity 0) en lugar de 0.15?
Técnicamente sí, cambia el intervalo de salida en useTransform a [0, 1]. En la práctica, partir de 0.15 mantiene visible la estructura del texto para que los usuarios perciban que hay contenido por revelar. Un cero total hace que el párrafo parezca vacío antes del scroll, lo que puede dar sensación de error.
¿Funciona en móvil?
La animación de scroll funciona en dispositivos táctiles, no hay dependencia del puntero. La sección está marcada como responsive:true y usa clamp() para el tamaño de fuente. El punto principal a tener en cuenta es que el minHeight de 80vh ocupa la mayor parte de la pantalla; en páginas con muy poco contenido puede resultar pesado.
¿Cómo manejo los espacios de puntuación en la división de palabras?
text.split(' ') es suficiente para una prosa estándar. Si tu texto tiene varios espacios o caracteres en blanco especiales, filtra las cadenas vacías con .filter(Boolean) después de la división. La puntuación pegada a una palabra (como una coma) permanece con esa palabra y se anima junto a ella, que es el comportamiento deseado.