Typewriter Animation

Animates text like a typewriter, with optional speed, cursor, and styling controls.

1. Copy the component file

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

typewriter-animation.jsx

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

const TypewriterAnimation = (props) => {
  const {
    text,
    words,
    hideCursor,
    blinkCursor = true,
    cursorVariant = "line",
    speed = 100,
  } = props;

  const [currentText, setCurrentText] = useState(text);
  const [currentWordIndex, setCurrentWordIndex] = useState(0);
  const [currentLetterIndex, setCurrentLetterIndex] = useState(-1);

  const wordsMap = useMemo(() => {
    let wordsArr = null;
    if(Array.isArray(words)) {
      wordsArr = words;
    } else {
      wordsArr = text.split(" ").filter(Boolean);
    }
    return wordsArr
    .map((word, wordIndex, arr) => {
      const temp = [{
        ...(typeof word === "string" ? ({
          letters: word.split(""),
          speed: speed,
        }) : ({
          letters: word.text.split(""),
          ...word,
        }))
      }];
      if (wordIndex !== (arr.length - 1)) { 
        temp.push({
          letters: [" "],
          speed,
        });
      }
      return temp;
    }).flat();
  }, [text, words, speed]);

  const { isLastWord, isLastLetter } = useMemo(() => {
    const letters = wordsMap[currentWordIndex].letters;
    const isLastWord = currentWordIndex === (wordsMap.length-1);
    const isLastLetter = currentLetterIndex === (letters.length-1);
    return {
      isLastWord,
      isLastLetter,
    };
  }, [wordsMap, currentWordIndex, currentLetterIndex]);

  useEffect(() => {
    const timeoutDuration = wordsMap[currentWordIndex].speed ?? speed;
    const timeout = setTimeout(() => {
      if (isLastWord && isLastLetter) {
        return;
      } else if (isLastLetter) {
        setCurrentWordIndex(currentWordIndex+ 1);
        setCurrentLetterIndex(0);
      } else {
        setCurrentLetterIndex(currentLetterIndex + 1);
      }
    }, timeoutDuration);
    return () => {
      clearTimeout(timeout);
    };
  }, [wordsMap, currentWordIndex, currentLetterIndex, speed]);

  const isBlinkCursor = (
    blinkCursor && (
      isLastLetter === -1 ||
      (isLastWord && isLastLetter)
    )
  );

  if(currentText !== text) {
    setCurrentText(text);
    setCurrentWordIndex(0);
    setCurrentLetterIndex(-1);
  }

  return (
    <span 
      aria-label={text}
      className={styles["typewriter-animation"]}
    >
      {wordsMap.map((word, wordIndex) => (
        <span 
          key={`word-${wordIndex}`}
          aria-hidden={true}
          hidden={wordIndex > currentWordIndex}
          {...word.slotProps}
        >
          {word.letters.map((letter, letterIndex) => (
            <span 
              key={`letter-${letterIndex}`}
              aria-hidden={true}
              className={styles["letter"]}
              hidden={(
                wordIndex === currentWordIndex && 
                letterIndex > currentLetterIndex
              )}
            >
              {letter}
            </span>
          ))}
        </span>
      ))}
      <span
        aria-hidden={true}
        className={[
          styles["cursor"],
          styles[cursorVariant],
          isBlinkCursor ? styles["blink"] : ""
        ].join(" ")}
        hidden={hideCursor}
      >
        {cursorVariant === "underscore" ? "_" : <>&nbsp;</>}
      </span>
    </span>
  );
};

export default memo(TypewriterAnimation);

2. Copy the CSS module file

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

typewriter-animation.module.css

.typewriter-animation {
  --cursor-color: currentcolor;

  .cursor {
    display: inline-block;
    background: var(--cursor-color);

    &.blink {
      animation: cursor-blink-keyframes 1500ms ease-in-out infinite;
    }

    &.line {
      width: 4px;
    }

    &.block {
      width: 12px;
    }

    &.underscore {
      background: transparent;
      animation-name: cursor-underscore-blink-keyframes;
    }
  }
}

@keyframes cursor-blink-keyframes {
  0%,
  25%,
  75%,
  100% {
    background: transparent;
  }

  30%,
  70% {
    background: var(--cursor-color);
  }
}

@keyframes cursor-underscore-blink-keyframes {
  0%,
  25%,
  75%,
  100% {
    color: transparent
  }

  30%,
  70% {
    color: var(--cursor-color);
  }
}

3. Use the component

Now you can import and use the component anywhere in your project.

typewriter-animation-preview.jsx

import TypewriterAnimation from "@/components/text-effects/typewriter-animation/typewriter-animation";

const TypewriterAnimationPreview = () => {
  return (
    <h2>
      <TypewriterAnimation
        text="Typing the future, live."
        speed={75}
      />
    </h2>
  )
};

export default TypewriterAnimationPreview;

Cursor Types



typewriter-animation

import TypewriterAnimation from "@/components/text-effects/typewriter-animation/typewriter-animation";

const TypewriterAnimationCursorsPreview = () => {
  const text = "Typing the future, live.";

  return (
    <div>
      <h2>
        <TypewriterAnimation
          text={text}
          cursorVariant="line"
        />
      </h2>
      <br />
      <h2>
        <TypewriterAnimation
          text={text}
          cursorVariant="block"
        />
      </h2>
      <br />
       <h2>
        <TypewriterAnimation
          text={text}
          cursorVariant="underscore"
        />
      </h2>
    </div>
  )
};

export default TypewriterAnimationCursorsPreview;

Customize word speed and style

typewriter-animation

import TypewriterAnimation from "@/components/text-effects/typewriter-animation/typewriter-animation";

const TypewriterAnimationCustomizeWordsPreview = () => {
  const words = [
    { 
      text: "Hello", 
      speed: 500, 
      slotProps: { 
        style: { color: "#22d6d7" } 
      },
    },
    { 
      text: "World", 
      speed: 50, 
      slotProps: { 
        style: { color: "#de287d" } 
      },
    },
  ];

  return (
    <h2>
      <TypewriterAnimation
        words={words}
      />
    </h2>
  )
};

export default TypewriterAnimationCustomizeWordsPreview;

Props

Configure the component with the following props:

PropTypeRequiredDefaultDescription
textstringNoSimple text to type. Use for basic usage instead of words.
wordsArray<{ text: string; speed?: number; slotProps?: object }>NoAdvanced configuration for typing multiple entries. text defines the actual content to display, speed lets you control typing speed per word (overriding the global speed), and slotProps allows custom styling or additional props (e.g., color, className) for that specific word—giving you fine-grained visual and behavioral control.
blinkCursorbooleanNotrueEnables cursor blinking animation.
cursorVariant"line" | "block" | "underscore"No"line"Controls the visual style of the cursor.
speednumberNo100Default typing speed in milliseconds (used if not overridden in words).