Typewriter Text
Typewriter Text is a React and Tailwind CSS component from Kokonut UI, licensed MIT. Copy it and paste it into your project.
Kokonut UI
MIT
other
What it is
Typewriter Text is a React and Tailwind CSS component from Kokonut UI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i motion
Licence
This component comes from Kokonut UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Typewriter Text
"use client";
/**
* @author: @dorianbaffier
* @description: Typewriter
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { useEffect, useRef, useState } from "react";
type TypewriterSequence = {
text: string;
deleteAfter?: boolean;
pauseAfter?: number;
};
type TypewriterTitleProps = {
sequences?: TypewriterSequence[];
typingSpeed?: number;
startDelay?: number;
autoLoop?: boolean;
loopDelay?: number;
deleteSpeed?: number;
pauseBeforeDelete?: number;
naturalVariance?: boolean;
};
const DEFAULT_SEQUENCES: TypewriterSequence[] = [
{ text: "Typewriter", deleteAfter: true },
{ text: "Multiple Words", deleteAfter: true },
{ text: "Auto Loop", deleteAfter: false },
];
export default function TypewriterTitle({
sequences = DEFAULT_SEQUENCES,
typingSpeed = 50,
startDelay = 200,
autoLoop = true,
loopDelay = 1000,
deleteSpeed = 30,
pauseBeforeDelete = 1000,
naturalVariance = true,
}: TypewriterTitleProps) {
const [displayText, setDisplayText] = useState("");
const sequenceIndexRef = useRef(0);
const charIndexRef = useRef(0);
const isDeletingRef = useRef(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// Initialize with the sequences provided
const sequencesRef = useRef(sequences);
useEffect(() => {
sequencesRef.current = sequences;
}, [sequences]);
useEffect(() => {
const getTypingDelay = () => {
if (!naturalVariance) {
return typingSpeed;
}
// More natural human typing pattern
const random = Math.random();
// 10% chance of a longer pause (thinking/hesitation)
if (random < 0.1) {
return typingSpeed * 2;
}
// 10% chance of a burst (fast typing)
if (random > 0.9) {
return typingSpeed * 0.5;
}
// Standard variance (+/- 40%)
const variance = 0.4;
const min = typingSpeed * (1 - variance);
const max = typingSpeed * (1 + variance);
return Math.random() * (max - min) + min;
};
const runTypewriter = () => {
const currentSequence = sequencesRef.current[sequenceIndexRef.current];
if (!currentSequence) {
return;
}
if (isDeletingRef.current) {
if (charIndexRef.current > 0) {
charIndexRef.current -= 1;
setDisplayText(currentSequence.text.slice(0, charIndexRef.current));
timeoutRef.current = setTimeout(runTypewriter, deleteSpeed);
} else {
isDeletingRef.current = false;
const isLastSequence =
sequenceIndexRef.current === sequencesRef.current.length - 1;
if (isLastSequence && autoLoop) {
timeoutRef.current = setTimeout(() => {
sequenceIndexRef.current = 0;
runTypewriter();
}, loopDelay);
} else if (!isLastSequence) {
timeoutRef.current = setTimeout(() => {
sequenceIndexRef.current += 1;
runTypewriter();
}, 100); // Quick transition to next word
}
}
} else if (charIndexRef.current < currentSequence.text.length) {
charIndexRef.current += 1;
setDisplayText(currentSequence.text.slice(0, charIndexRef.current));
timeoutRef.current = setTimeout(runTypewriter, getTypingDelay());
} else {
const pauseDuration = currentSequence.pauseAfter ?? pauseBeforeDelete;
if (currentSequence.deleteAfter) {
timeoutRef.current = setTimeout(() => {
isDeletingRef.current = true;
runTypewriter();
}, pauseDuration);
} else {
const isLastSequence =
sequenceIndexRef.current === sequencesRef.current.length - 1;
if (isLastSequence && autoLoop) {
timeoutRef.current = setTimeout(() => {
sequenceIndexRef.current = 0;
charIndexRef.current = 0;
setDisplayText("");
runTypewriter();
}, loopDelay);
} else if (!isLastSequence) {
timeoutRef.current = setTimeout(() => {
sequenceIndexRef.current += 1;
charIndexRef.current = 0;
setDisplayText("");
runTypewriter();
}, pauseDuration);
}
}
}
};
// Start the loop
timeoutRef.current = setTimeout(runTypewriter, startDelay);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [
// Only restart effect if timing configs change.
// We use sequencesRef for content to avoid restarting on array reference change.
typingSpeed,
deleteSpeed,
pauseBeforeDelete,
autoLoop,
loopDelay,
startDelay,
naturalVariance,
]);
return (
<div className="relative mx-auto w-full max-w-4xl py-24">
<div className="relative z-10 flex flex-col items-center justify-center text-center">
<motion.div
animate={{ opacity: 1 }}
className="flex items-center gap-1 font-mono text-4xl text-black tracking-tight md:text-6xl dark:text-white"
initial={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
<span className="inline-block min-h-[1.2em] min-w-[0.5em]">
{displayText}
</span>
<motion.span
animate={{
opacity: [1, 1, 0, 0],
}}
className="inline-block h-[1em] w-[3px] bg-black dark:bg-white"
transition={{
duration: 1,
repeat: Number.POSITIVE_INFINITY,
repeatType: "loop",
ease: "linear",
}}
/>
</motion.div>
</div>
</div>
);
}