NEW
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
Learn more
Skip to content
Docs

Wave Reveal

Reveal letter or word one by one with a wave effect & optional blur effect.

Loading...

Installation

CLI

pnpm dlx shadcn@latest add https://animata.design/r/text/wave-reveal.json

Manual

Add to your CSS

@keyframes reveal-up {
  0% { opacity: 0; transform: translateY(80%); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes reveal-down {
  0% { opacity: 0; transform: translateY(-80%); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes content-blur {
  0% { filter: blur(0.3rem); }
  100% { filter: blur(0); }
}
@theme {
  --ease-minor-spring: cubic-bezier(0.18, 0.89, 0.82, 1.04);
}

Run the following command

It will create a new file called wave-reveal.tsx inside the components/animata/text directory.

touch components/animata/text/wave-reveal.tsx

Paste the code

Open the newly created file and paste the following code:

import type { ReactNode } from "react";
 
import { cn } from "@/lib/utils";
 
interface WaveRevealProps {
  /**
   * The text to animate
   */
  text: string;
 
  /**
   * Additional classes for the container
   */
  className?: string;
 
  /**
   * The direction of the animation
   * @default "down"
   */
  direction?: "up" | "down";
 
  /**
   * The mode of the animation
   * @default "letter"
   */
  mode?: "letter" | "word";
 
  /**
   * Duration of the animation
   * E.g. 2000ms
   */
  duration?: string;
 
  /**
   * If true, the text will apply a blur effect as seen in WWDC.
   */
  blur?: boolean;
 
  letterClassName?: string;
 
  /**
   * Delay for each letter/word in ms
   */
  delay?: number;
}
 
interface ReducedValue extends Pick<WaveRevealProps, "direction" | "mode"> {
  nodes: ReactNode[];
  offset: number;
  duration: number | string;
  delay: number;
  blur?: boolean;
  className?: string;
  wordsLength: number;
  textLength: number;
}
 
const Word = ({
  isWordMode,
  word,
  index,
  offset,
  delay,
  duration,
  className,
}: Pick<ReducedValue, "delay" | "duration" | "offset"> & {
  index: number;
  className: string;
  isWordMode: boolean;
  word: string;
  length: number;
}) => {
  if (isWordMode) {
    return word;
  }
 
  return (
    <>
      {word.split("").map((letter, letterIndex) => {
        return (
          <span
            key={`${letter}_${letterIndex}_${index}`}
            className={cn({
              [className]: !isWordMode,
            })}
            style={{
              animationDuration: `${duration}`,
              animationDelay: createDelay({
                index: letterIndex,
                offset,
                delay,
              }),
            }}
          >
            {letter === " " ? "\u00A0" : letter}
          </span>
        );
      })}
    </>
  );
};
 
const createDelay = ({
  offset,
  index,
  delay,
}: Pick<ReducedValue, "offset" | "delay"> & {
  index: number;
}) => {
  return `${delay + (index + offset) * 50}ms`;
};
 
const createAnimatedNodes = (args: ReducedValue, word: string, index: number): ReducedValue => {
  const { nodes, offset, wordsLength, textLength, mode, direction, duration, delay, blur } = args;
 
  const isWordMode = mode === "word";
  const isUp = direction === "up";
  const length = isWordMode ? wordsLength : textLength;
  const isLast = index === length - 1;
 
  const className = cn(
    "inline-block opacity-0 transition ease-in-out fill-mode-forwards",
    {
      // Determine the animation direction
      "animate-[reveal-down]": !isUp && !blur,
      "animate-[reveal-up]": isUp && !blur,
      "animate-[reveal-down,content-blur]": !isUp && blur,
      "animate-[reveal-up,content-blur]": isUp && blur,
    },
    args.className,
  );
  const node = (
    <span
      key={`word_${index}`}
      className={cn("contents", {
        [className]: isWordMode,
      })}
      style={
        isWordMode
          ? {
              animationDuration: `${duration}`,
              animationDelay: createDelay({
                index,
                offset,
                delay,
              }),
            }
          : undefined
      }
    >
      <Word
        isWordMode={isWordMode}
        word={word}
        index={index}
        offset={offset}
        duration={duration}
        className={className}
        length={length}
        delay={delay}
      />
      {!isLast && "\u00A0"}
    </span>
  );
 
  return {
    ...args,
    nodes: [...nodes, node],
    offset: offset + (isWordMode ? 1 : word.length + 1),
  };
};
 
export default function WaveReveal({
  text,
  direction = "down",
  mode = "letter",
  className,
  duration = "2000ms",
  delay = 0,
  blur = true,
  letterClassName,
}: WaveRevealProps) {
  if (!text) {
    return null;
  }
 
  const words = text.trim().split(/\s/);
 
  const { nodes } = words.reduce<ReducedValue>(createAnimatedNodes, {
    nodes: [],
    offset: 0,
    wordsLength: words.length,
    textLength: text.length,
    direction,
    mode,
    duration: duration ?? 60,
    delay: delay ?? 0,
    blur,
    className: letterClassName,
  });
 
  return (
    <div
      className={cn(
        "relative flex flex-wrap justify-center whitespace-pre px-2 text-4xl font-medium md:px-6 md:text-7xl",
        className,
      )}
    >
      {nodes}
      <div className="sr-only">{text}</div>
    </div>
  );
}

Credits

Built by hari