Scroll-driven word-by-word or character-by-character text reveal animation using Motion.
Made by LéoBlur-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 a 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
"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:
"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
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | — | The text to animate |
as | HTML tag | "p" | Container element type |
splitBy | "words" | "characters" | `"words" | Split animation mode |
threshold | number (0–1) | 0.85 | Viewport position where reveal completes |
className | string | — | Extra CSS classes for the container |