Typewriter
Typewriter effect for text. Supports html and markdown.
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 React, { useEffect, useRef, useState } from 'react';
import Markdown, { Components } from 'react-markdown';
type FlashCursorProps = React.HTMLAttributes<HTMLSpanElement> & { hideCursor?: boolean };
export const FlashCursor = ({ hideCursor, className, ...props }: FlashCursorProps) => {
return (
<span
className={cn(
'animate-blink border-r-4 border-r-primary/60 pl-1 text-transparent',
className,
hideCursor && 'hidden',
)}
{...props}
/>
);
};
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.
Playground for ChatGPT
Here is the ChatGPT integration playground. Please enter your token to test. Don't worry, the API endpoint does not store your token. You can check the API source code.
If you still have concerns, you can clone the project and test it locally.
Default is using gpt-4o-mini
model.
Properties
Property | Type | Default |
---|---|---|
text | string | string |
typeSpeed | number | 33 |
renderMarkdown | boolean | |
markdownComponents | Components | |
onComplete | () => void | |
className | string |