Text Reveal

Scroll-driven word-by-word or character-by-character text reveal animation using Motion.

Made by Léo
Open in

Blur-Driven Text Animation

Every word appears exactly when you need it — viewport-triggered, blur-based, zero configuration required.

Word by Word

Elegant animations that reveal your message progressively. Each word fades in with subtle blur-to-sharp transition as it enters the viewport. Perfect for engaging storytelling and creating compelling visual narratives.

Character by Character

For maximum precision, animate character by character.

The viewport-triggered animation feels natural and immersive, adapting perfectly to different screen sizes.

— Design System Documentation

demo-text-reveal.tsx
"use client";

import { TextReveal } from "@/components/unlumen-ui/text-reveal";

interface TextRevealDemoProps {
  splitBy?: "words" | "characters";
  staggerDelay?: number;
  duration?: number;
  once?: boolean;
}

export const TextRevealDemo = ({
  splitBy = "words",
  staggerDelay = 0.05,
  duration = 0.5,
  once = true,
}: TextRevealDemoProps) => {
  return (
    <div className="w-full max-w-3xl mx-auto px-6 py-24 space-y-24">
      {/* Hero Section */}
      <div className="space-y-6">
        <TextReveal
          text="Blur-Driven Text Animation"
          as="h1"
          splitBy={splitBy}
          staggerDelay={staggerDelay}
          duration={duration}
          once={once}
          className="text-5xl md:text-6xl font-bold tracking-tight text-foreground"
        />

        <TextReveal
          text="Every word appears exactly when you need it — viewport-triggered, blur-based, zero configuration required."
          as="p"
          splitBy={splitBy}
          staggerDelay={staggerDelay}
          duration={duration}
          once={once}
          className="text-lg md:text-xl font-medium text-muted-foreground leading-relaxed"
        />
      </div>

      {/* Word by Word Section */}
      <div className="space-y-6">
        <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
          Word by Word
        </h2>

        <TextReveal
          text="Elegant animations that reveal your message progressively. Each word fades in with a subtle blur-to-sharp transition as it enters the viewport. Perfect for engaging storytelling and creating compelling visual narratives."
          as="p"
          splitBy={splitBy}
          staggerDelay={staggerDelay}
          duration={duration}
          once={once}
          className="text-2xl md:text-3xl font-medium text-foreground leading-relaxed"
        />
      </div>

      {/* Character by Character Section */}
      <div className="space-y-6">
        <h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider">
          Character by Character
        </h2>

        <TextReveal
          text="For maximum precision, animate character by character."
          as="p"
          splitBy={splitBy}
          staggerDelay={staggerDelay}
          duration={duration}
          once={once}
          className="text-xl md:text-2xl font-medium text-foreground leading-relaxed"
        />
      </div>

      {/* Large Quote */}
      <div className="space-y-6 pt-12 border-t border-border">
        <TextReveal
          text="The viewport-triggered animation feels natural and immersive, adapting perfectly to different screen sizes."
          as="h3"
          splitBy={splitBy}
          staggerDelay={staggerDelay}
          duration={duration}
          once={once}
          className="text-3xl md:text-4xl font-bold text-foreground leading-relaxed italic"
        />

        <p className="text-muted-foreground">— Design System Documentation</p>
      </div>
    </div>
  );
};

export default TextRevealDemo;

Installation

Install the following dependencies:

Install the following registry dependencies:

Copy and paste the following code into your project:

components/unlumen-ui/text-reveal.tsx
"use client";

import * as React from "react";
import { motion, useInView } from "motion/react";

import { cn } from "@/lib/utils";

export interface TextRevealProps {
  /** The text to animate. */
  text: string;
  /**
   * HTML tag for the container element.
   * @default "p"
   */
  as?: keyof React.JSX.IntrinsicElements;
  /**
   * Split mode: word-by-word or character-by-character.
   * @default "words"
   */
  splitBy?: "words" | "characters";
  /**
   * Delay between each word/character animation in seconds.
   * @default 0.05
   */
  staggerDelay?: number;
  /**
   * Duration of each unit's animation in seconds.
   * @default 0.5
   */
  duration?: number;
  /**
   * Trigger the animation only once.
   * @default true
   */
  once?: boolean;
  className?: string;
}

export function TextReveal({
  text,
  as: Tag = "p",
  splitBy = "words",
  staggerDelay = 0.05,
  duration = 0.5,
  once = true,
  className,
}: TextRevealProps) {
  const ref = React.useRef<HTMLElement>(null);
  const isInView = useInView(ref, { once, margin: "0px 0px -10% 0px" });

  const units =
    splitBy === "words"
      ? text
          .split(/\s+/)
          .map((w, i, arr) => (i < arr.length - 1 ? w + "\u00A0" : w))
      : text.split("");

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const AnyTag = Tag as any;

  return (
    <AnyTag
      ref={ref}
      className={cn("leading-relaxed", className)}
      aria-label={text}
    >
      {units.map((unit, i) => (
        <motion.span
          key={i}
          initial={{ opacity: 0.1, filter: "blur(8px)" }}
          animate={
            isInView
              ? { opacity: 1, filter: "blur(0px)" }
              : { opacity: 0.1, filter: "blur(8px)" }
          }
          transition={{
            duration,
            delay: i * staggerDelay,
            ease: "easeOut",
          }}
          style={{ display: "inline-block" }}
          className="will-change-[opacity,filter]"
        >
          {unit}
        </motion.span>
      ))}
    </AnyTag>
  );
}

Update the import paths to match your project setup.

Usage

import { TextReveal } from "@/components/unlumen-ui/text-reveal";

<TextReveal text="Your text animates as you scroll" />;

Split Modes

Choose between word-by-word or character-by-character reveal animations.

{
  /* Word-by-word (default) */
}
<TextReveal text="This reveals word by word as you scroll" splitBy="words" />;

{
  /* Character-by-character */
}
<TextReveal text="This reveals character by character" splitBy="characters" />;

Custom Container

Use any HTML tag as the container element.

<TextReveal
  text="This is styled as a heading"
  as="h2"
  className="text-4xl font-bold"
/>

Threshold Control

Control how far into the viewport the reveal completes (0–1). Lower values reveal sooner.

{
  /* Reveals earlier when entering viewport */
}
<TextReveal text="Reveals at 50% into viewport" threshold={0.5} />;

{
  /* Reveals later (default is 0.85) */
}
<TextReveal text="Reveals at 85% into viewport" threshold={0.85} />;

Props

PropTypeDefaultDescription
textstringThe text to animate
asHTML tag"p"Container element type
splitBy"words" | "characters"`"words"Split animation mode
thresholdnumber (0–1)0.85Viewport position where reveal completes
classNamestringExtra CSS classes for the container

Built by Léo from Unlumen :3

Last updated: 3/15/2026