Browse the library
Components / Text
Text reveal
FreeHeadline that reveals word by word with a blur, fade and gentle rise as it scrolls into view.
View as Markdownmotion
components/facade/text-reveal.tsx
"use client";
import * as React from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
import type { Variants } from "motion/react";
import { cn } from "@/lib/utils";
export interface TextRevealProps {
/** The text to reveal, split on whitespace. */
text: string;
/** Element to render. Defaults to "h2". */
as?: "h1" | "h2" | "h3" | "p";
/** Delay in seconds before the first word. Defaults to 0. */
delay?: number;
/** Seconds between each word. Defaults to 0.06. */
stagger?: number;
/** Reveal only once. Defaults to true. */
once?: boolean;
className?: string;
/** Class applied to every word span. */
wordClassName?: string;
}
const word: Variants = {
hidden: { opacity: 0, y: "0.45em", filter: "blur(10px)" },
visible: {
opacity: 1,
y: "0em",
filter: "blur(0px)",
transition: { duration: 0.7, ease: [0.22, 1, 0.36, 1] },
},
};
const wordReduced: Variants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.3 } },
};
export function TextReveal({
text,
as = "h2",
delay = 0,
stagger = 0.06,
once = true,
className,
wordClassName,
}: TextRevealProps) {
const ref = React.useRef<HTMLElement>(null);
const inView = useInView(ref, { once, amount: 0.4 });
const reduce = useReducedMotion();
const words = text.split(/\s+/).filter(Boolean);
const Tag = as;
return (
<Tag ref={ref as React.Ref<HTMLHeadingElement & HTMLParagraphElement>} className={cn(className)}>
<span className="sr-only">{text}</span>
<motion.span
aria-hidden
initial="hidden"
animate={inView ? "visible" : "hidden"}
transition={{ staggerChildren: reduce ? 0 : stagger, delayChildren: delay }}
className="inline"
>
{words.map((w, i) => (
<React.Fragment key={`${w}-${i}`}>
<motion.span
variants={reduce ? wordReduced : word}
className={cn("inline-block will-change-[transform,filter,opacity]", wordClassName)}
>
{w}
</motion.span>
{i < words.length - 1 ? " " : null}
</React.Fragment>
))}
</motion.span>
</Tag>
);
}
export default TextReveal;app/page.tsx
import { TextReveal } from "@/components/facade/text-reveal";
export default function Demo() {
return (
<div className="w-full max-w-2xl py-6">
<TextReveal
as="p"
text="Hollis and Reed Realty"
className="font-mono text-xs uppercase tracking-[0.2em] text-brand"
stagger={0.08}
/>
<TextReveal
as="h2"
text="Find a home in Denver that feels like it was built around your life."
delay={0.2}
className="mt-4 font-display text-4xl font-semibold leading-[1.05] tracking-tight text-foreground sm:text-[52px]"
/>
<TextReveal
as="p"
text="Local agents, honest pricing, and off-market listings you will not see on the big portals."
delay={0.75}
stagger={0.025}
className="mt-5 max-w-lg text-base leading-relaxed text-muted-foreground sm:text-lg"
/>
</div>
);
}Installation
$
npx shadcn@latest add https://facadeui.com/r/text-reveal.jsonUsage
import { TextReveal } from "@/components/facade/text-reveal";
export default function Page() {
return <TextReveal />;
}Props
TextRevealProps is exported, so you can extend or wrap it. Every prop has a default; <TextReveal /> renders the example above.
| Prop | Type | Description |
|---|---|---|
| text* | string | The text to reveal, split on whitespace. |
| as | "h1" | "h2" | "h3" | "p" | Element to render. Defaults to "h2". |
| delay | number | Delay in seconds before the first word. Defaults to 0. |
| stagger | number | Seconds between each word. Defaults to 0.06. |
| once | boolean | Reveal only once. Defaults to true. |
| className | string | |
| wordClassName | string | Class applied to every word span. |
* required
Ask your agent
With the Facade MCP server connected, this prompt is enough:
Add the Facade text reveal to this page and adapt the copy to our business.