Docs
Wave Reveal

Wave Reveal

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

Installation

Update tailwind.config.js

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

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 { 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}
          </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-all ease-minor-spring 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({
        [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 && " "}
    </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-black md:px-6 md:text-7xl",
        className,
      )}
    >
      {nodes}
      <div className="sr-only">{text}</div>
    </div>
  );
}

Credits

Built by hari