Text Animation

Create dynamic text effects by staggering letters or words with configurable motion and timing, great for hero sections, promotional content, or onboarding screens.

text-animation-preview.jsx

import TextAnimation from "@/components/text-effects/text-animation/text-animation";

const TextAnimationPreview = () => {
  return (
    <TextAnimation
      text="Transform static text into fluid animation"
      variant="slideDown"
    />
  );
};

export default TextAnimationPreview;

Installation

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

Terminal / Console

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

1. Copy the component file

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

text-animation.jsx

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

const TextAnimationUnit = memo((props) => {
  const {
    variant,
    animationDelayMs,
    animationDurationMs,
    children
  } = props;

  const ref = useRef();

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

  return (
    <span
      aria-hidden={true}
      className={styles[variant]}
      ref={ref}
      style={{
        "--animation-delay": `${animationDelayMs}ms`,
      }}
    >
      {children === " " ? (
        <>&nbsp;</>
      ) : children}
    </span>
  );
});

const TextAnimation = (props) => {
  const {
    text,
    variant = "fadeIn",
    unit = "letter",
    stagger = 20,
    delay = 0,
    className,
    style,
    ...restProps
  } = props;

  const animationDurationMs = useMemo(() => (
    ({
      letter: 250,
      word: 400,
      text: 500,
    })[unit] ?? 500
  ), [unit]);

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

  return (
    <span
      {...restProps}
      className={[
        className,
        styles["text-animation"],
      ].join(" ")}
      style={{
        ...style,
        "--animation-duration": `${animationDurationMs}ms`
      }}
    >
      {textUnits.map((entry, entryIndex) => (
        <TextAnimationUnit
          key={`text-${entry}-${entryIndex}`}
          animationDelayMs={delay + (stagger * entryIndex)}
          animationDurationMs={animationDurationMs}
          variant={variant}
        >
          {entry}
        </TextAnimationUnit>
      ))}
      <span
        className={styles["sr-only"]}
      >
        {text}
      </span>
    </span>
  );
};

export default memo(TextAnimation);

2. Copy the CSS module file

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

text-animation.module.css

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

  position: relative;

  >span {
    display: inline-block;
    opacity: 0;
    animation-duration: var(--animation-duration);
    animation-timing-function: ease-in-out;
    animation-fill-mode: forwards;
    animation-delay: var(--animation-delay);

    &.fadeIn {
      animation-name: text-fade-keyframes;
    }

    &.slideUp {
      animation-name: text-slide-up-keyframes;
    }

    &.slideDown {
      animation-name: text-slide-down-keyframes;
    }

    &.slideLeft {
      animation-name: text-slide-left-keyframes;
    }

    &.slideRight {
      animation-name: text-slide-right-keyframes;
    }

    &.zoomIn {
      animation-name: text-zoom-in-keyframes;
    }

    &.zoomOut {
      animation-name: text-zoom-out-keyframes;
    }

    &.blurIn {
      animation-name: text-blur-keyframes;
    }

    &.done {
      opacity: 1;
      filter: blur(0px);
      transform: translate(0px, 0px) scale(1);
      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-fade-keyframes {
  0% {
    opacity: 0;
  }

  100% {
    opacity: 1;
  }
}

@keyframes text-slide-up-keyframes {
  0% {
    opacity: 0;
    transform: translateY(20px);
  }

  100% {
    opacity: 1;
    transform: translateY(0px);
  }
}

@keyframes text-slide-down-keyframes {
  0% {
    opacity: 0;
    transform: translateY(-20px);
  }

  100% {
    opacity: 1;
    transform: translateY(0px);
  }
}

@keyframes text-slide-left-keyframes {
  0% {
    opacity: 0;
    transform: translateX(20px);
  }

  100% {
    opacity: 1;
    transform: translateX(0px);
  }
}

@keyframes text-slide-right-keyframes {
  0% {
    opacity: 0;
    transform: translateX(-20px);
  }

  100% {
    opacity: 1;
    transform: translateX(0px);
  }
}

@keyframes text-zoom-in-keyframes {
  0% {
    opacity: 0;
    transform: scale(0.25);
  }

  100% {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes text-zoom-out-keyframes {
  0% {
    opacity: 0;
    transform: scale(1.5);
  }

  100% {
    opacity: 1;
    transform: scale(1);
  }
}

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

  100% {
    opacity: 1;
    filter: blur(0px);
  }
}

Examples

Discover how supported props transform components to match your needs.

Variants

Create beautiful text animations with ease

text-animation

import TextAnimation from "@/components/text-effects/text-animation/text-animation";
import { useState } from "react";

const TextAnimationVariantsPreview = () => {
  const variants = [
    "fadeIn",
    "slideUp",
    "slideDown",
    "slideLeft",
    "slideRight",
    "zoomIn",
    "zoomOut",
    "blurIn",
  ];
  const [variant, setVariant] = useState(variants[0]);

  return (
    <div>
      <div style={controlStyles}>
        <label
          htmlFor="text-animation-variants-input"
        >
          Variants: 
        </label>
        <select
          id="text-animation-variants-input"
          value={variant}
          onChange={(e) => setVariant(e.target.value)}
        >
          {variants.map(variant => (
            <option
              key={variant}
              value={variant}
            >
              {variant}
            </option>
          ))}
        </select>
      </div>
      <TextAnimation
        text="Create beautiful text animations with ease"
        variant={variant}
      />
    </div>
  );
};

const controlStyles = {
  textAlign: "center",
  marginBottom: "8px",
};

export default TextAnimationVariantsPreview;

Unit & Stagger

Craft delightful experiences with animated text

text-animation

import TextAnimation from "@/components/text-effects/text-animation/text-animation";
import { useState } from "react";

const TextAnimationUnitAndStaggerPreview = () => {
  const units = [
    "letter",
    "word",
    "text",
  ];

  const [unit, setUnit] = useState(units[0]);

  const stagger = ({
    letter: 20,
    word: 50,
    text: 0,
  })[unit];

  return (
    <div>
      <div style={controlStyles}>
        <label
          htmlFor="text-animation-unit-input"
        >
          Unit: 
        </label>
        <select
          id="text-animation-unit-input"
          value={unit}
          onChange={(e) => setUnit(e.target.value)}
        >
          {units.map(unit => (
            <option 
              key={unit}
              value={unit}
            >
              {unit}
            </option>
          ))}
        </select>
      </div>
      <TextAnimation
        text="Craft delightful experiences with animated text"
        unit={unit}
        stagger={stagger}
        variant="slideUp"
      />
    </div>
  );
};

const controlStyles = {
  textAlign: "center",
  marginBottom: "8px",
};

export default TextAnimationUnitAndStaggerPreview;

Delay

Good things take time to appear

text-animation

import TextAnimation from "@/components/text-effects/text-animation/text-animation";

const TextAnimationDelayPreview = () => {
  return (
    <TextAnimation
      text="Good things take time to appear"
      variant="slideDown"
      delay={5000}
    />
  );
};

export default TextAnimationDelayPreview;

Props

Configure the component with the following props:

PropTypeRequiredDefaultDescription
textstringYesThe text content to be animated.
variant"fadeIn"
"slideUp"
"slideDown"
"slideLeft"
"slideRight"
"zoomIn"
"zoomOut"
"blurIn"
No"fadeIn"Animation style applied to the text.
unit"letter" | "word" | "text"No"letter"Determines how the text is split and animated.
staggernumberNo20Time delay between each animated unit (in milliseconds).
delaynumberNo0Delay before the animation starts (in milliseconds).
classNamestringNoAdditional CSS classes for styling.
styleReact.CSSPropertiesNoInline styles applied to the container.