Installation

Use the following command to install the component:

$ pnpm dlx shadcn@latest add https://ui.sanjid.in/r/text-reveal.json

Install the following dependencies:

$ pnpm add motion

Add util file

lib/utils.ts
tsx
import { ClassValue, clsx } from "clsx";import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {  return twMerge(clsx(inputs));}

Copy and paste the following code into your project.

components/ui/text-reveal.tsx
tsx
"use client";
import { motion, useReducedMotion, Variants } from "motion/react";import { cn } from "@/lib/utils";
interface TextRevealProps {  text: string;  variant?: "blur-sm" | "chat" | "pop";  className?: string;  delayPerWord?: number;  duration?: number;}
const EASE_OUT = [0.23, 1, 0.32, 1] as const;
export const TextReveal = ({  text,  variant = "blur-sm",  className,  delayPerWord = 0.05,  duration = 0.28,}: TextRevealProps) => {  const reduce = useReducedMotion();  const words = text.trim().split(/\s+/);  // Cap total stagger so long strings stay under ~300ms + duration  const stagger = Math.min(delayPerWord, 0.28 / Math.max(words.length, 1));  const dur = Math.min(duration, 0.28);
  if (reduce) {    return (      <span        className={cn(          "inline-flex flex-wrap items-baseline text-(--color-ink)",          className,        )}      >        {words.map((word, i) => (          <span key={i} className="inline-block whitespace-pre">            {word}            {i < words.length - 1 ? "\u00A0" : null}          </span>        ))}      </span>    );  }
  const variants: Record<NonNullable<TextRevealProps["variant"]>, Variants> = {    "blur-sm": {      hidden: { opacity: 0, filter: "blur(4px)", y: 6 },      visible: (i: number) => ({        opacity: 1,        filter: "blur(0px)",        y: 0,        transition: {          delay: i * stagger,          duration: dur,          ease: EASE_OUT,        },      }),    },    chat: {      hidden: { opacity: 0, x: -6 },      visible: (i: number) => ({        opacity: 1,        x: 0,        transition: {          delay: i * stagger,          duration: dur,          ease: EASE_OUT,        },      }),    },    pop: {      hidden: { opacity: 0, scale: 0.95 },      visible: (i: number) => ({        opacity: 1,        scale: 1,        transition: {          delay: i * stagger,          type: "spring",          bounce: 0,          duration: dur,        },      }),    },  };
  return (    <motion.span      initial="hidden"      animate="visible"      className={cn(        "inline-flex flex-wrap items-baseline text-(--color-ink)",        className,      )}    >      {words.map((word, i) => (        <motion.span          key={i}          custom={i}          variants={variants[variant]}          className="inline-block whitespace-pre"        >          {word}          {i < words.length - 1 ? "\u00A0" : null}        </motion.span>      ))}    </motion.span>  );};

Update the import paths to match your project setup.

Basic Usage

tsx
<TextReveal text="Hello, world!" variant="blur" />

Advanced Usage

tsx
<TextReveal  text="Hello, world!"  variant="blur"  className="text-2xl font-bold"  delayPerWord={0.1}  duration={1}/>

  • Blur: Blur the text
  • Chat: Chat-like effect
  • Pop: Pop-like effect

tsx
// Blur<TextReveal text="Hello, world!" variant="blur" className="text-2xl font-bold" delayPerWord={0.1} duration={1} />
// Chat<TextReveal text="Hello, world!" variant="chat" className="text-2xl font-bold" delayPerWord={0.1} duration={1} />
// Pop<TextReveal text="Hello, world!" variant="pop" className="text-2xl font-bold" delayPerWord={0.1} duration={1} />

Props

PropTypeRequiredDefaultDescription
textstringNoThe text to reveal
variantstringNoThe variant of the text reveal
classNamestringNoThe class name of the text reveal
delayPerWordnumberNoThe delay per word
durationnumberNoThe duration of the text reveal

Best Practices

  1. Cap stagger so long strings stay near the UI budget (≤280ms per word + capped delay).
  2. Prefer ease-out / bounce: 0 springs — never backOut overshoot.
  3. Under reduced motion, render plain text with no stagger.
  4. Use for occasional/first-view copy, not high-frequency UI chrome.