Scroll Reveal Text

Text animation tied to scroll position - reveals as you scroll, reverses when scrolling back up. Creates dynamic, interactive text effects perfect for storytelling and hero sections.

Blur Reveal

Text blurs into clarity as you scroll down, and blurs back out when scrolling up.

Character Reveal

Each character reveals individually based on scroll progress.

Slide Reveal

Text slides into view tied to scroll position.

Installation

npm install framer-motion
pnpm install framer-motion
yarn add framer-motion
bun add framer-motion

Copy and paste the following code into your project.

'use client';import { useRef, useMemo } from 'react';import { motion, useScroll, useTransform } from 'framer-motion';interface ScrollRevealTextProps {  text: string;  className?: string;  revealType?: 'blur' | 'fade' | 'slide' | 'scale' | 'characters';  scrollOffset?: ['start 0.9', 'start 0.3'] | readonly [string, string];  staggerDelay?: number;  blurAmount?: number;  slideDistance?: number;}export const ScrollRevealText = ({  text,  className = '',  revealType = 'blur',  scrollOffset = ['start 0.9', 'start 0.3'],  staggerDelay = 0.03,  blurAmount = 10,  slideDistance = 30,}: Readonly<ScrollRevealTextProps>) => {  const containerRef = useRef<HTMLDivElement>(null);  const { scrollYProgress } = useScroll({    target: containerRef,    offset: scrollOffset as any,  });  // For word/character-based reveals  const words = useMemo(() => text.split(' '), [text]);  const characters = useMemo(() => text.split(''), [text]);  // Blur reveal transforms  const blurValue = useTransform(    scrollYProgress,    [0, 1],    [`blur(${blurAmount}px)`, 'blur(0px)'],  );  const blurOpacity = useTransform(scrollYProgress, [0, 0.5, 1], [0.3, 0.7, 1]);  // Fade reveal transforms  const fadeOpacity = useTransform(scrollYProgress, [0, 1], [0, 1]);  // Slide reveal transforms  const slideY = useTransform(    scrollYProgress,    [0, 1],    [slideDistance, 0],  );  const slideOpacity = useTransform(scrollYProgress, [0, 0.3, 1], [0, 0.5, 1]);  // Scale reveal transforms  const scale = useTransform(scrollYProgress, [0, 1], [0.8, 1]);  const scaleOpacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 0.5, 1]);  // Render based on reveal type  if (revealType === 'blur') {    return (      <motion.div        ref={containerRef}        className={`relative ${className}`}        style={{          filter: blurValue,          opacity: blurOpacity,        }}      >        {text}      </motion.div>    );  }  if (revealType === 'fade') {    return (      <motion.div        ref={containerRef}        className={`relative ${className}`}        style={{          opacity: fadeOpacity,        }}      >        {text}      </motion.div>    );  }  if (revealType === 'slide') {    return (      <motion.div        ref={containerRef}        className={`relative ${className}`}        style={{          y: slideY,          opacity: slideOpacity,        }}      >        {text}      </motion.div>    );  }  if (revealType === 'scale') {    return (      <motion.div        ref={containerRef}        className={`relative ${className}`}        style={{          scale,          opacity: scaleOpacity,        }}      >        {text}      </motion.div>    );  }  // Characters reveal - each character reveals based on scroll  if (revealType === 'characters') {    return (      <div ref={containerRef} className={`relative ${className}`}>        <span className="sr-only">{text}</span>        <span aria-hidden>          {characters.map((char, index) => (            <CharacterReveal              key={index}              char={char}              index={index}              total={characters.length}              scrollYProgress={scrollYProgress}              staggerDelay={staggerDelay}              blurAmount={blurAmount}            />          ))}        </span>      </div>    );  }  return (    <div ref={containerRef} className={`relative ${className}`}>      {text}    </div>  );};// Component for individual character revealinterface CharacterRevealProps {  char: string;  index: number;  total: number;  scrollYProgress: any;  staggerDelay: number;  blurAmount: number;}const CharacterReveal = ({  char,  index,  total,  scrollYProgress,  staggerDelay,  blurAmount,}: CharacterRevealProps) => {  // Calculate when this character should start and end revealing  const staggerOffset = index * staggerDelay;  const startProgress = Math.min(staggerOffset, 0.5);  const endProgress = Math.min(staggerOffset + 0.3, 1);  const opacity = useTransform(    scrollYProgress,    [startProgress, endProgress],    [0, 1],  );  const blur = useTransform(    scrollYProgress,    [startProgress, endProgress],    [blurAmount, 0],  );  const y = useTransform(    scrollYProgress,    [startProgress, endProgress],    [10, 0],  );  // Handle the filter transform  const filterBlur = useTransform(blur, (v) => `blur(${v}px)`);  return (    <motion.span      className="inline-block"      style={{        opacity,        filter: filterBlur,        y,        display: char === ' ' ? 'inline' : 'inline-block',        minWidth: char === ' ' ? '0.3em' : 'auto',      }}    >      {char === ' ' ? '\u00A0' : char}    </motion.span>  );};export default ScrollRevealText;

Props

Prop

Type

Examples

// Blur reveal (default)
<ScrollRevealText
  text="As you scroll, I reveal"
  revealType="blur"
  blurAmount={15}
/>

// Character-by-character reveal
<ScrollRevealText
  text="Character by character"
  revealType="characters"
  staggerDelay={0.02}
/>

// Slide in from below
<ScrollRevealText
  text="Sliding into view"
  revealType="slide"
  slideDistance={50}
/>

// Fade in
<ScrollRevealText
  text="Fading in"
  revealType="fade"
/>

// Scale in
<ScrollRevealText
  text="Scaling up"
  revealType="scale"
/>

// Custom scroll trigger points
<ScrollRevealText
  text="Earlier trigger"
  scrollOffset={['start 0.95', 'start 0.5']}
/>