Typewriter

Typewriter effect for text. Supports html and markdown.

Lorem Ipsum is simply dummy text of the printing and typesetting industry

Installation

1

Install the package if you do not have it.

npm install react-markdown
2

Copy and paste the following code into your project.

'use client';
import { cn } from '@/lib/utils';
import { useEffect, useRef, useState } from 'react';
import Markdown, { Components } from 'react-markdown';

type TypewriterProps = {
  text?: string;
  typeSpeed?: number;
  className?: string;
  onComplete?: () => void;
  renderMarkdown?: boolean;
  markdownComponents?: Components;
};

export const Typewriter = ({
  text = '',
  typeSpeed = 33,
  onComplete,
  className,
  renderMarkdown,
  markdownComponents,
}: TypewriterProps) => {
  const [displayedText, setDisplayedText] = useState('');
  const intervalRef = useRef<NodeJS.Timeout | null>(null);
  const onCompleteRef = useRef(onComplete); // Ref to store the latest onComplete

  // Keep onComplete callback reference up-to-date without causing effect re-runs
  useEffect(() => {
    onCompleteRef.current = onComplete;
  }, [onComplete]);
  useEffect(() => {
    /***
     * Each time htmlString changes,
     * only add new characters from the end of the currently displayed text.
     */
    const startTyping = () => {
      let currentIndex = displayedText.length;
      intervalRef.current = setInterval(() => {
        if (currentIndex < text.length) {
          // Only add new characters, do not reset old text
          setDisplayedText(text.slice(0, currentIndex + 1));
          currentIndex++;
        } else {
          if (intervalRef.current) {
            clearInterval(intervalRef.current);
          }
          onCompleteRef.current?.();
        }
      }, typeSpeed);
    };

    // If there is new text, start typing animation
    if (text.length > displayedText.length) {
      // Start typing immediately, no need to wait for delay
      startTyping();
    }
    // If targetText decreases or resets, consider handling it自行處理
    return () => {
      if (intervalRef.current) {
        clearInterval(intervalRef.current);
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [text, typeSpeed]);

  if (renderMarkdown) {
    return (
      <div className={className}>
        <Markdown components={markdownComponents}>{displayedText}</Markdown>
      </div>
    );
  }

  return (
    <span
      className={cn('whitespace-pre-wrap leading-7', className)}
      dangerouslySetInnerHTML={{ __html: displayedText }}
    />
  );
};
3

Copy following animation to tailwind.config.js


      keyframes: {
        'blink-caret': {
          '0%, 100%': { opacity: '0' },
          '50%': { opacity: '1' },
        },
      },
      'blink-caret': {
        '0%, 100%': { opacity: '0' },
        '50%': { opacity: '1' },
      },
4

Update the import paths to match your project setup.

Markdown

Following markdown style is using tailwindcss-typography

Here is shopping list:

  • apple
  • banana
  • orange

Lorem Ipsum is simply dummy text of

Properties

PropertyTypeDefault
text
string
string
typeSpeed
number
33
renderMarkdown
boolean
markdownComponents
Components
onComplete
() => void
className
string
Buy Me A Coffee