Rolling Letters

Displays a word where each character animates with a vertical rolling effect, creating a dynamic text reveal.

Usage

rolling-letters-animation-preview.tsx
import RollingLettersAnimation from "@/registry/text-effects/rolling-letters-animation/rolling-letters-animation";

const RollingLettersAnimationPreview = () => {
  return (
    <RollingLettersAnimation
      text="MosaicUI"
      className="font-mono font-extrabold text-neutral-900 dark:text-white text-5xl uppercase"
      blockWidth={32}
      blockHeight={48}
      blockGap={2}
    />
  );
};

export default RollingLettersAnimationPreview;

Component

Install the following packages before using this component.

Motion
Tailwind CSS
rolling-letters-animation.tsx
import { memo, useMemo } from "react";
import { motion } from "motion/react";
import cn from "@/utils/cn";

export type RollingLettersAnimationProps = {
  text: string;
  duration?: number,
  blockWidth?: number;
  blockHeight: number;
  blockGap?: number;
  className?: string,
  style?: React.CSSProperties,
  blockClassName?: string,
  blockStyle?: React.CSSProperties,
} & React.ComponentProps<"span">;

const RollingLettersAnimation = (
  props: RollingLettersAnimationProps,
) => {
  const {
    text,
    blockWidth,
    blockHeight = 16,
    blockGap = 0,
    duration = 2,
    className,
    style,
    blockClassName,
    blockStyle,
    ...restProps
  } = props;

  const _duration = Math.max(0.1, duration);

  const upperCaseLetterSet = useMemo(() => (
    Array.from({
      length: 26,
    }).map((_, i) => (
      String.fromCharCode(65 + i)
    ))
  ), []);

  const lowerCaseLetterSet = useMemo(() => (
    Array.from({
      length: 26,
    }).map((_, i) => (
      String.fromCharCode(97 + i)
    ))
  ), []);

  const getRandomSeries = (
    exceptLetter: string,
  ) => {
    const isUpperCase = exceptLetter === exceptLetter.toUpperCase();
    const letterSet = isUpperCase ? upperCaseLetterSet : lowerCaseLetterSet;
    const lettersExceptGivenLetter = letterSet.filter(l => l !== exceptLetter);
    const shuffledLetterSet = lettersExceptGivenLetter.sort(() => (
      Math.random() - Math.random()
    ));
    return shuffledLetterSet;
  };

  return (
    <span
      {...restProps}
      className={cn(
        "relative flex",
        "[gap:var(--block-gap)]",
        className,
      )}
      style={{
        ...style,
        "--block-height": `${blockHeight}px`,
        "--block-gap": `${blockGap}px`,
        ...(blockWidth && ({ "--block-width": `${blockWidth}px` })),
      } as React.CSSProperties}
    >
      {[...text].map((letter, letterIndex) => {
        const series = getRandomSeries(letter);
        const isReverse = (letterIndex % 2) === 0;
        if (isReverse) {
          series.unshift(letter);
        } else {
          series.push(letter);
        }
        return (
          <span
            aria-hidden={true}
            key={`letter-window-${letter}-${letterIndex}`}
            className={cn(
              "inline-block overflow-hidden leading-[1]",
              "[width:var(--block-width,initial)]",
              "[height:var(--block-height)]",
            )}
            style={{
              "--total-rolling-letters": `${series.length - 1}`,
            } as React.CSSProperties}
          >
            <motion.span
              className="inline-block"
              style={{
                y: 0,
                marginTop: isReverse ? `${(series.length - 1) * -blockHeight}px` : 0, 
              }}
              animate={{
                y: (series.length - 1) * blockHeight * (isReverse ? 1 :  -1),
              }}
              transition={{
                ease: "easeInOut",
                duration: _duration,
              }}
            >
              {series.map((l, i) => (
                <span
                  aria-hidden={true}
                  key={`letter-block-${l}-${i}`}
                  className={cn(
                    "flex justify-center items-end overflow-hidden",
                    "[width:var(--block-width,initial)]",
                    "[height:var(--block-height)]",
                    blockClassName,
                  )}
                  style={blockStyle}
                >
                  {l === " " ? <>&nbsp;</> : l}
                </span>
              ))}
            </motion.span>
          </span>
        )
      })}
      <span className="sr-only">
        {text}
      </span>
    </span>
  );
};

export default memo(RollingLettersAnimation);

Component API

PropTypeRequiredDefaultDescription
textstringYesThe text to display and animate with the rolling letter effect.
blockWidthnumberNo16Width of each letter container. Helps maintain consistent horizontal spacing when fonts are not monospace. Typically set to match the text font-size.
blockHeightnumberYes16Height of each letter container. Helps maintain consistent vertical alignment and spacing. Typically set to match the text font-size.
blockGapnumberNo0Horizontal gap between adjacent letter containers. Useful for fine-tuning the spacing between characters.
durationnumberNo2Total animation duration in seconds. Minimum value: 0.1.
classNamestringNoAdditional CSS class applied to the root container element.
styleReact.CSSPropertiesNoInline styles applied to the root container element.
blockClassNamestringNoCSS class applied to each individual letter block element. Useful for custom letter styling or effects.
blockStyleReact.CSSPropertiesNoInline styles applied to each letter block element.

Support the Project

If you find this component useful, consider starring the repository on GitHub. Found a bug or have a suggestion? Open an issue to help improve it.