NEW
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
shadcn registry is live·
Learn more
Skip to content
Docs

Typing Text

Creates a typing effect for given text

Loading...

Installation

CLI

pnpm dlx shadcn@latest add https://animata.design/r/text/typing-text.json

Manual

Run the following command

It will create a new file called typing-text.tsx inside the components/animata/text directory.

mkdir -p components/animata/text && touch components/animata/text/typing-text.tsx

Paste the code

Open the newly created file and paste the following code:

import { type ReactNode, useEffect, useMemo, useState } from "react";
 
import { cn } from "@/lib/utils";
 
interface TypingTextProps {
  /**
   * Text to type
   */
  text: string;
 
  /**
   * Delay between typing each character or word (smooth mode) in milliseconds
   * @default 32
   */
  delay?: number;
 
  /**
   * If true, the text will be erased after typing and then typed again.
   */
  repeat?: boolean;
 
  /**
   * Custom cursor to show at the end of the text.
   * Applies only when `smooth` is false.
   */
  cursor?: ReactNode;
 
  /**
   * Additional classes to apply to the container
   */
  className?: string;
 
  /**
   * If true, the container will grow to fit the text as it types
   */
  grow?: boolean;
 
  /**
   * Number of characters to keep always visible
   */
  alwaysVisibleCount?: number;
 
  /**
   * If true, the typing effect will be smooth instead of typing one character at a time.
   * Looks better for multiple words.
   */
  smooth?: boolean;
 
  /**
   * Time to wait before starting the next cycle of typing
   * Applies only when `repeat` is true.
   *
   * @default 1000
   *
   */
  waitTime?: number;
 
  /**
   * Callback function to be called when the typing is complete
   */
  onComplete?: () => void;
 
  /**
   * If true, the cursor will be hidden after typing is complete
   * @default false
   */
  hideCursorOnComplete?: boolean;
}
 
function Blinker() {
  const [show, setShow] = useState(true);
  useEffect(() => {
    const interval = setInterval(() => {
      setShow((prev) => !prev);
    }, 500);
    return () => clearInterval(interval);
  }, []);
  return <span className={show ? "" : "opacity-0"}>|</span>;
}
 
function SmoothEffect({
  words,
  index,
  alwaysVisibleCount,
}: {
  words: string[];
  index: number;
  alwaysVisibleCount: number;
}) {
  return (
    <div className="flex flex-wrap whitespace-pre">
      {words.map((word, wordIndex) => {
        return (
          <span
            key={wordIndex}
            className={cn("transition-opacity duration-300 ease-in-out", {
              "opacity-100": wordIndex < index,
              "opacity-0": wordIndex >= index + alwaysVisibleCount,
            })}
          >
            {word}
            {wordIndex < words.length && <span>&nbsp;</span>}
          </span>
        );
      })}
    </div>
  );
}
 
function NormalEffect({
  text,
  index,
  alwaysVisibleCount,
}: {
  text: string;
  index: number;
  alwaysVisibleCount: number;
}) {
  return <>{text.slice(0, Math.max(index, Math.min(text.length, alwaysVisibleCount ?? 1)))}</>;
}
 
enum TypingDirection {
  Forward = 1,
  Backward = -1,
}
 
function CursorWrapper({
  visible,
  children,
  waiting,
}: {
  visible?: boolean;
  waiting?: boolean;
  children: ReactNode;
}) {
  const [on, setOn] = useState(true);
  useEffect(() => {
    const interval = setInterval(() => {
      setOn((prev) => !prev);
    }, 100);
    return () => clearInterval(interval);
  }, []);
 
  if (!visible || (!on && !waiting)) {
    return null;
  }
 
  return children;
}
 
function Type({
  text,
  repeat,
  cursor,
  delay,
  grow,
  className,
  alwaysVisibleCount,
  smooth,
  waitTime = 1000,
  onComplete,
  hideCursorOnComplete,
}: TypingTextProps) {
  const [index, setIndex] = useState(0);
  const [direction, setDirection] = useState<TypingDirection>(TypingDirection.Forward);
  const [isComplete, setIsComplete] = useState(false);
 
  const words = useMemo(() => text.split(/\s+/), [text]);
  const total = smooth ? words.length : text.length;
 
  useEffect(() => {
    // eslint-disable-next-line prefer-const
    let interval: NodeJS.Timeout;
 
    const startTyping = () => {
      setIndex((prevDir) => {
        if (direction === TypingDirection.Backward && prevDir === TypingDirection.Forward) {
          clearInterval(interval);
        } else if (direction === TypingDirection.Forward && prevDir === total - 1) {
          clearInterval(interval);
        }
        return prevDir + direction;
      });
    };
 
    interval = setInterval(startTyping, delay);
    return () => clearInterval(interval);
  }, [total, direction, delay]);
 
  useEffect(() => {
    let timeout: NodeJS.Timeout;
 
    if (index >= total && repeat) {
      timeout = setTimeout(() => {
        setDirection(-1);
      }, waitTime);
    }
 
    if (index <= 0 && repeat) {
      timeout = setTimeout(() => {
        setDirection(1);
      }, waitTime);
    }
    return () => clearTimeout(timeout);
  }, [index, total, repeat, waitTime]);
 
  useEffect(() => {
    if (index === total && !repeat) {
      setIsComplete(true);
      onComplete?.();
    }
  }, [index, total, repeat, onComplete]);
 
  const waitingNextCycle = index === total || index === 0;
 
  return (
    <div className={cn("relative font-mono", className)}>
      {!grow && <div className="invisible">{text}</div>}
      <div
        className={cn({
          "absolute inset-0 h-full w-full": !grow,
        })}
      >
        {smooth ? (
          <SmoothEffect words={words} index={index} alwaysVisibleCount={alwaysVisibleCount ?? 1} />
        ) : (
          <NormalEffect text={text} index={index} alwaysVisibleCount={alwaysVisibleCount ?? 1} />
        )}
        <CursorWrapper
          waiting={waitingNextCycle}
          visible={Boolean(!smooth && cursor && (!hideCursorOnComplete || !isComplete))}
        >
          {cursor}
        </CursorWrapper>
      </div>
    </div>
  );
}
 
export default function TypingText({
  text,
  repeat = true,
  cursor = <Blinker />,
  delay = 32,
  className,
  grow = false,
  alwaysVisibleCount = 1,
  smooth = false,
  waitTime,
  onComplete,
  hideCursorOnComplete = false,
}: TypingTextProps) {
  return (
    <Type
      key={text}
      delay={delay ?? 32}
      waitTime={waitTime ?? 1000}
      grow={grow}
      repeat={repeat}
      text={text}
      cursor={cursor}
      className={className}
      smooth={smooth}
      alwaysVisibleCount={alwaysVisibleCount}
      onComplete={onComplete}
      hideCursorOnComplete={hideCursorOnComplete}
    />
  );
}

Credits

Built by hari