Text reveal
Text that rises out of a blur, word by word, as it scrolls into view.
$
pnpm dlx shadcn@latest add https://lodeui.com/r/text-reveal.json
components/examples/text-reveal-demo.tsx
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { TextReveal } from "@/components/ui/text-reveal";
export default function TextRevealDemo() {
const [k, setK] = React.useState(0);
return (
<div className="flex max-w-xl flex-col items-center gap-6 text-center">
<TextReveal key={k} as="h2" text="Interfaces that feel finished on day one." className="text-4xl font-semibold tracking-tight" />
<TextReveal key={`c${k}`} by="char" delay={0.4} text="Copy it. Own it. Ship it." className="font-mono text-sm text-muted-foreground" />
<Button size="sm" variant="secondary" onClick={() => setK((x) => x + 1)}>Replay</Button>
</div>
);
}When to use
- Hero headings and section titles.
When not to use
- Body copy and anything people need to read immediately.
Installation
$
pnpm dlx shadcn@latest add https://lodeui.com/r/text-reveal.json1Install the dependencies
npm install motion2Copy the source into your project
components/ui/text-reveal.tsx"use client"; import * as React from "react"; import { motion, useInView, useReducedMotion } from "motion/react"; import { cn } from "@/lib/utils"; export interface TextRevealProps { text: string; /** Reveal per word or per character. */ by?: "word" | "char"; as?: "h1" | "h2" | "h3" | "p" | "span"; delay?: number; /** Replay every time it scrolls into view. */ repeat?: boolean; className?: string; } /** Text that rises out of a blur, word by word, when it enters the viewport. */ export function TextReveal({ text, by = "word", as: Tag = "p", delay = 0, repeat, className }: TextRevealProps) { const ref = React.useRef<HTMLElement>(null); const inView = useInView(ref, { once: !repeat, margin: "-10% 0px" }); const reduce = useReducedMotion(); const parts = by === "word" ? text.split(/(\s+)/) : [...text]; const MotionTag = motion[Tag]; return ( <MotionTag ref={ref as never} className={cn(className)} aria-label={text}> {parts.map((p, i) => /^\s+$/.test(p) ? ( <span key={i}>{p}</span> ) : ( <motion.span key={i} aria-hidden className="inline-block" initial={reduce ? false : { opacity: 0, y: "0.35em", filter: "blur(6px)" }} animate={inView || reduce ? { opacity: 1, y: 0, filter: "blur(0px)" } : undefined} transition={{ delay: delay + i * (by === "word" ? 0.04 : 0.015), duration: 0.5, ease: [0.2, 0.8, 0.2, 1] }} > {p} </motion.span> ), )} </MotionTag> ); }
Usage
Import it from @/components/ui/text-reveal after installing.
example.tsx
import { TextReveal } from "@/components/ui/text-reveal";
export function Hero() {
return <TextReveal as="h1" text="Ship product UI at platform speed." className="text-5xl font-semibold" />;
}API reference
TextReveal
Words or characters rise and unblur when scrolled into view.
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | – | The text. |
| as | "h1" | "h2" | "h3" | "p" | "span" | "p" | Element. |
| by | "word" | "char" | "word" | Unit. |
| delay | number | 0 | Seconds. |
| repeat | boolean | – | Replay on every entry. |
Accessibility
- The element has aria-label with the full text; pieces are aria-hidden.
- Shown at once under reduced motion.
Motion
- Each unit fades up from 0.35em with a blur.
Responsive behavior
- Wraps naturally.
Performance
- One span per unit; prefer by="word" for long text.
Notes for AI
- Use as="h1" for hero headings so the outline stays right.
This page as Markdown, for assistants and coding agents:
/components/text-reveal/markdownThe full library index for assistants is at /llms.txt.
Lode222 doc pagesllms.txt