Typewriter Text
Realistic typing animation with blinking cursor, variable speed, and delete-retype loop
animationtexttypewritertyping
Install dependencies
$npm install framer-motionPreview
Source Code
"use client";
import { useEffect, useState, useCallback } from "react";
import { motion } from "framer-motion";
interface TypewriterTextProps {
words?: string[];
typingSpeed?: number;
deletingSpeed?: number;
pauseDuration?: number;
cursorChar?: string;
cursorClassName?: string;
className?: string;
}
export default function TypewriterText({
words = ["Hello World"],
typingSpeed = 80,
deletingSpeed = 50,
pauseDuration = 1500,
cursorChar = "|",
cursorClassName = "",
className = "",
}: TypewriterTextProps) {
const [displayText, setDisplayText] = useState("");
const [wordIndex, setWordIndex] = useState(0);
const [isDeleting, setIsDeleting] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const currentWord = words[wordIndex];
const tick = useCallback(() => {
if (isPaused) return;
if (!isDeleting) {
if (displayText.length < currentWord.length) {
setDisplayText(currentWord.slice(0, displayText.length + 1));
} else {
setIsPaused(true);
setTimeout(() => {
setIsPaused(false);
setIsDeleting(true);
}, pauseDuration);
}
} else {
if (displayText.length > 0) {
setDisplayText(currentWord.slice(0, displayText.length - 1));
} else {
setIsDeleting(false);
setWordIndex((prev) => (prev + 1) % words.length);
}
}
}, [displayText, isDeleting, isPaused, currentWord, words.length, pauseDuration]);
useEffect(() => {
if (isPaused) return;
const speed = isDeleting ? deletingSpeed : typingSpeed;
// Add slight randomness for realism
const jitter = Math.random() * 40 - 20;
const timeout = setTimeout(tick, speed + jitter);
return () => clearTimeout(timeout);
}, [tick, isDeleting, isPaused, typingSpeed, deletingSpeed]);
return (
<span className={`inline-flex items-baseline ${className}`}>
<span>{displayText}</span>
<motion.span
animate={{ opacity: [1, 0, 1] }}
transition={{ duration: 0.8, repeat: Infinity, ease: "linear", times: [0, 0.5, 1] }}
className={`ml-[1px] inline-block font-light ${cursorClassName || "text-current"}`}
>
{cursorChar}
</motion.span>
</span>
);
}