# Text Reveal

Words rise out of a blur one after another, when the text scrolls into view.

**Category:** Text  
**Exports:** `TextReveal`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/text-reveal.json
```
The file lands at `components/ui/text-reveal.tsx`; import from `@/components/ui/text-reveal`.

## API — Text Reveal
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `text` | `string` | — |  |
| `as` | `ElementType` | `"p"` |  |
| `by` | `"word" \| "char"` | `"word"` |  |
| `stagger` | `number` | `0.04` |  |
| `once` | `boolean` | `true` |  |

## Example — Text Reveal

```tsx
import { TextReveal } from "@/components/ui/text-reveal";

export default function TextRevealDemo({
  text = "Interfaces should move like objects, not slides.",
  by = "word",
  stagger = 0.04,
}: {
  text?: string;
  by?: "word" | "char";
  stagger?: number;
}) {
  return (
    <div className="max-w-lg text-center">
      <TextReveal as="h3" text={text} by={by} stagger={stagger} className="text-3xl font-semibold tracking-tight text-balance" />
      <TextReveal by="char" stagger={0.012} delay={0.5} text="Press Replay to watch it again." className="mt-4 text-muted-foreground" />
    </div>
  );
}
```

## Source — components/ui/text-reveal.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type TextRevealProps = {
  text: string;
  as?: React.ElementType;
  by?: "word" | "char";
  /** Seconds between pieces. */
  stagger?: number;
  delay?: number;
  once?: boolean;
  className?: string;
};

export function TextReveal({ text, as: Tag = "p", by = "word", stagger = 0.04, delay = 0, once = true, className }: TextRevealProps) {
  const reduce = useReducedMotion();
  const words = text.split(" ");

  const piece = {
    hidden: reduce ? { opacity: 0 } : { opacity: 0, y: "0.35em", filter: "blur(8px)" },
    visible: { opacity: 1, y: 0, filter: "blur(0px)", transition: { type: "spring" as const, stiffness: 260, damping: 30 } },
  };

  return (
    <Tag className={className} aria-label={text}>
      <motion.span
        aria-hidden
        className="inline"
        initial="hidden"
        whileInView="visible"
        viewport={{ once, margin: "0px 0px -10% 0px" }}
        transition={{ staggerChildren: reduce ? 0 : stagger, delayChildren: delay }}
      >
        {words.map((word, wi) => (
          <React.Fragment key={wi}>
            {by === "word" ? (
              <motion.span variants={piece} className="inline-block will-change-transform">
                {word}
              </motion.span>
            ) : (
              <span className="inline-block whitespace-nowrap">
                {word.split("").map((ch, ci) => (
                  <motion.span key={ci} variants={piece} className={cn("inline-block will-change-transform")}>
                    {ch}
                  </motion.span>
                ))}
              </span>
            )}
            {wi < words.length - 1 && " "}
          </React.Fragment>
        ))}
      </motion.span>
    </Tag>
  );
}
```