Text Emerge

Animates text by gradually turning blurry letters into clear ones, word or letter by letter.

text-emerge-animation-preview.jsx

import TextEmergeAnimation from "@/components/text-effects/text-emerge-animation/text-emerge-animation";

const text = (`
A subtle motion to guide your attention. Nothing loud, nothing distracting — just a quiet transition that makes the interface feel alive. 
Good animation isn't decoration; it's a gentle cue that helps you understand where you are and what happens next.
`
);


const TextEmergeAnimationPreview = () => {
  return (
    <div 
      style={{
        width: "calc(100% - 64px)",
        height: "calc(100% - 64px)",
      }}
    >
      <h2>
        <TextEmergeAnimation 
          text={text}
          speed={50}
        />
      </h2>
    </div>
  );
};

export default TextEmergeAnimationPreview;

Installation

Run the command from your project root directory (the folder that contains package.json).

Terminal / Console

npx mosaicui-cli@latest text-effects/text-emerge-animation

1. Copy the component file

Create a new file called text-emerge-animation.jsx in your reusable components folder (for example /src/components/) and paste the following code into it.

text-emerge-animation.jsx

import { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import styles from "./text-emerge-animation.module.css";

const TextEmergeUnit = (props) => {
  const {
    animationDurationMs,
    children
  } = props;

  const ref = useRef();

  useEffect(() => {
    const timeoutId = setTimeout(() => {
      ref.current?.classList.add(styles["visible"]);
    }, animationDurationMs);
    return () => {
      clearTimeout(timeoutId);
    };
  }, [animationDurationMs]);

  return (
    <span
      aria-hidden={true}
      ref={ref}
    >
      {children}
    </span>
  )
};

const TextEmergeAnimation = (props) => {
  const {
    text,
    type = "word",
    speed = 100,
    className,
    style,
    ...restProps
  } = props;

  const animationDurationMs = 800;

  const [currentIndex, setCurrentIndex] = useState(-1);

  const textMapping = useMemo(() => (
    text
    .split(" ")
    .filter(Boolean)
    .map(word => {
      if (type === "word") {
        return [word, " "];
      } else {
        return [
          ...word
            .split("")
            .filter(Boolean),
          " "
        ];
      }
    })
    .flat()
  ), [text, type]);

  useLayoutEffect(() => {
    const timeout = setTimeout(() => {
      const isLast = currentIndex === (textMapping.length - 1);
      if (isLast) return;
      setCurrentIndex(currentIndex + 1);
    }, speed);
    return () => {
      clearTimeout(timeout);
    }
  }, [currentIndex, textMapping, speed]);

  useEffect(() => {
    setCurrentIndex(-1);
  }, [text, type]);

  return (
    <span
      {...restProps}
      className={[
        className,
        styles["text-emerge-animation"]
      ].join(" ")}
      style={{
        ...style,
        "--animation-duration": `${animationDurationMs}ms`
      }}
    >
      {textMapping.map((entry, entryIndex) => (
        entryIndex <= currentIndex ? (
          <TextEmergeUnit
            key={`text-${entryIndex}`}
            animationDurationMs={animationDurationMs}
          >
            {entry}
          </TextEmergeUnit>
        ) : null
      ))}
      <span
        className={styles["sr-only"]}
      >
        {text}
      </span>
    </span>
  )
};

export default memo(TextEmergeAnimation);

2. Copy the CSS module file

In the same folder, create a file called text-emerge-animation.module.css and paste the following CSS.

text-emerge-animation.module.css

.text-emerge-animation {
  --animation-duration: 0;

  position: relative;

  > span {
    opacity: 0;
    filter: blur(20px);
    animation: text-emerge-keyframes var(--animation-duration) ease-in-out forwards;

    &.visible {
      opacity: 1;
      filter: blur(0px);
      animation: none;
    }
  }
  > .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
    animation: none;
  }
}

@keyframes text-emerge-keyframes {
  0% {
    opacity: 0;
    filter: blur(20px);
  }
  100% {
    opacity: 1;
    filter: blur(0px);
  }
}

Props

Configure the component with the following props:

PropTypeRequiredDefaultDescription
textstringYesThe text to display. Can be a word, sentence, or paragraph depending on type.
type"word" | "letter"No"word"Determines the animation unit: "word" animates one word at a time, "letter" animates each letter individually.
speednumberNo100Typing speed in milliseconds per word or letter, depending on the type.
classNamestringNoOptional class name applied to the root container.
styleReact.CSSPropertiesNoInline styles applied to the root container.