Velora UI
All components

Text Reveal

Word-by-word blur reveal on scroll.

0.7 KB gzippedDeps: motion/reactReduced-motion safeBase UI & Radix compatible
Every word arrives exactly when it should

Installation

npx shadcn@latest add https://velora.colorlib.com/r/text-reveal.json

Installs with: motion

Source

"use client";

import { motion, useReducedMotion } from "motion/react";

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

interface TextRevealProps {
  /** Plain text — revealed word by word */
  text: string;
  className?: string;
  as?: "h1" | "h2" | "h3" | "p" | "span";
  delay?: number;
  /** Seconds between each word */
  stagger?: number;
  once?: boolean;
}

export function TextReveal({
  text,
  className,
  as: Tag = "span",
  delay = 0,
  stagger = 0.045,
  once = true,
}: TextRevealProps) {
  const reducedMotion = useReducedMotion();
  const words = text.split(" ");

  if (reducedMotion) {
    return <Tag className={cn(className)}>{text}</Tag>;
  }

  return (
    <Tag data-slot="text-reveal" className={cn(className)}>
      <span className="sr-only">{text}</span>
      <motion.span
        aria-hidden
        initial="hidden"
        whileInView="visible"
        viewport={{ once, margin: "0px 0px -10% 0px" }}
        transition={{ staggerChildren: stagger, delayChildren: delay }}
        className="inline"
      >
        {words.map((word, i) => (
          <motion.span
            key={`${word}-${i}`}
            className="inline-block will-change-transform"
            variants={{
              hidden: { opacity: 0, y: 12, filter: "blur(8px)" },
              visible: {
                opacity: 1,
                y: 0,
                filter: "blur(0px)",
                transition: { duration: 0.45, ease: [0.21, 0.47, 0.32, 0.98] },
              },
            }}
          >
            {word}
            {i < words.length - 1 && " "}
          </motion.span>
        ))}
      </motion.span>
    </Tag>
  );
}