This article will guide you through building a typing code animation component in React/Next.js. This component types out code with syntax highlighting, providing an engaging way to display code snippets for tutorials, presentations, or educational content. Incidentally, this setup is perfect for video recording, as the component is sized at 720p to maintain consistent formatting and high resolution for tutorial videos or presentations.
Component "CodeAnimation" is not available. Please define it in the MDX components.
Here below is a simple example of how to output text one character at a time. This can be run as a node script to see the effect.
const content = `Hello, App Router!
Hello World`;
let sentence = "";
const typeTest = async () => {
for (let index = 0; index < content.length; index++) {
sentence = content.slice(0, index + 1);
await new Promise((resolve) => setTimeout(resolve, 50));
console.log(sentence);
}
};
typeTest();
The component takes an array of code blocks and animates the typing of each block one character at a time. It includes an elapsed time display to track how long the animation has been running and provides an estimated total time for completion.
react-syntax-highlighter library with a customizable theme for highlighting code.Let's dive into the code and explain how each function and component works:
"use client";
// Type definition for a code block
interface CodeBlock {
time: number;
content: string;
delay: number;
}
// Code block configuration
const codeBlocks: CodeBlock[] = [
{
time: 1000,
content: `
export async function initializeClient() {
const session = await getSession();
return createApiClient(
process.env.API_BASE_URL ?? "https://default.api.url",
process.env.API_KEY ?? "default_api_key",
{
sessionData: {
getAll: () => session.getAllSessionData(),
setAll: (dataToSet) => {
for (const { key, value, options } of dataToSet) {
session.setSessionData(key, value, options);
}
},
clear: () => session.clearAllData(),
},
}
);
}
`,
delay: 50,
},
];
// Function to type each character in the content
async function typeCode(
content: string,
delay: number,
onUpdate: (newText: string) => void
): Promise<void> {
for (let index = 0; index < content.length; index++) {
onUpdate(content.slice(0, index + 1));
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// Helper function to calculate the total estimated time for animation
function calculateTotalAnimationTime(codeBlocks: CodeBlock[]): number {
const totalCharacters = codeBlocks.reduce(
(sum, block) => sum + block.content.length,
0
);
const totalDelay = codeBlocks.reduce((sum, block) => sum + block.time, 0);
const typingTime = totalCharacters * codeBlocks[0].delay;
return (totalDelay + typingTime) / 1000; // Convert to seconds
}
export default function CodeAnimation() {
const [displayedCode, setDisplayedCode] = useState<{ text: string }[]>([]);
const [started, setStarted] = useState<boolean>(false);
const [elapsedTime, setElapsedTime] = useState<number>(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const totalEstimatedTime = calculateTotalAnimationTime(codeBlocks);
// Timer management functions
const startTimer = () => {
timerRef.current = setInterval(
() => setElapsedTime((prev) => prev + 1),
1000
);
};
const stopTimer = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
// Function to start the animation
const startAnimation = () => {
setStarted(true);
setElapsedTime(0);
startTimer();
codeBlocks.forEach((block, idx) => {
setTimeout(async () => {
setDisplayedCode((prev) => [...prev, { text: "" }]);
await typeCode(block.content, block.delay, (newText) =>
setDisplayedCode((prev) => {
const updatedBlocks = [...prev];
updatedBlocks[idx].text = newText;
return updatedBlocks;
})
);
if (idx === codeBlocks.length - 1) stopTimer();
}, block.time);
});
};
return (
<div>
<div className="text-white p-2 rounded text-sm text-center">
Elapsed Time: {elapsedTime} seconds / Estimated Total Time:{" "}
{totalEstimatedTime.toFixed(2)} seconds
</div>
<div className="flex border items-center border-gray-800 h-[720px] w-[1280px] mx-auto">
<div className="code-animation-container w-full font-mono p-5 rounded-lg max-w-[1000px] mx-auto text-center text-xl">
{started ? (
displayedCode.map((block, idx) => (
<SyntaxHighlighter
key={idx}
language="javascript"
style={darcula}
className="custom-syntax-highlighter"
>
{block.text}
</SyntaxHighlighter>
))
) : (
<button
type="button"
onClick={startAnimation}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
>
Start Animation
</button>
)}
<style jsx>{`
:global(.custom-syntax-highlighter pre),
:global(.custom-syntax-highlighter code),
:global(.custom-syntax-highlighter) {
background: none !important;
}
`}</style>
</div>
</div>
</div>
);
}
typeCode FunctionThis function handles the animation of typing out the code block one character at a time. It iterates through the content string and calls the onUpdate callback with the updated string at each step, adding a delay between characters using setTimeout.
calculateTotalAnimationTime FunctionCalculates the estimated total time for the entire animation by summing up:
time) for each code block before it starts typing.This is displayed in the HUD to give users an idea of how long the animation will take.
startTimer: Starts the timer and updates elapsedTime every second.stopTimer: Stops the timer when the animation is complete.startAnimation FunctionThe main function that controls the animation flow:
time.displayedCode to reflect the currently typed characters.react-syntax-highlighter with the darcula theme for syntax highlighting.Most of the styling is handled using Tailwind CSS classes for consistency and maintainability. Custom styles are added using the :global rule to remove the background of the syntax highlighter.
To add a bit of realism, I added a sound effect that plays when the animation starts. This is done using the useEffect hook to initialize the audio object and the play method to start the sound.
"use client";
// Type definition for a code block
interface CodeBlock {
time: number;
content: string;
delay: number;
}
// Code block configuration
const codeBlocks: CodeBlock[] = [
{
time: 1000,
content: `
export async function initializeClient() {
const session = await getSession();
return createApiClient(
process.env.API_BASE_URL ?? "https://default.api.url",
process.env.API_KEY ?? "default_api_key",
{
sessionData: {
getAll: () => session.getAllSessionData(),
setAll: (dataToSet) => {
for (const { key, value, options } of dataToSet) {
session.setSessionData(key, value, options);
}
},
clear: () => session.clearAllData(),
},
}
);
}
`,
delay: 50,
},
];
// Function to type each character in the content
async function typeCode(
content: string,
delay: number,
onUpdate: (newText: string) => void
): Promise<void> {
for (let index = 0; index < content.length; index++) {
onUpdate(content.slice(0, index + 1));
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
// Helper function to calculate the total estimated time for animation
function calculateTotalAnimationTime(codeBlocks: CodeBlock[]): number {
const totalCharacters = codeBlocks.reduce(
(sum, block) => sum + block.content.length,
0
);
const totalDelay = codeBlocks.reduce((sum, block) => sum + block.time, 0);
const typingTime = totalCharacters * codeBlocks[0].delay;
return (totalDelay + typingTime) / 1000; // Convert to seconds
}
export default function CodeAnimation() {
const [displayedCode, setDisplayedCode] = useState<{ text: string }[]>([]);
const [started, setStarted] = useState<boolean>(false);
const [elapsedTime, setElapsedTime] = useState<number>(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const totalEstimatedTime = calculateTotalAnimationTime(codeBlocks);
const typingSoundRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
// Initialize the audio object
typingSoundRef.current = new Audio("/typing.mp3");
}, []);
// Timer management functions
const startTimer = () => {
timerRef.current = setInterval(
() => setElapsedTime((prev) => prev + 1),
1000
);
};
const stopTimer = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
// Function to start the animation
const startAnimation = () => {
setStarted(true);
setElapsedTime(0);
startTimer();
// Play typing sound
if (typingSoundRef.current) {
typingSoundRef.current.loop = true; // Loop the sound while typing
typingSoundRef.current.play().catch((err) => {
console.error("Error playing sound:", err);
});
}
codeBlocks.forEach((block, idx) => {
setTimeout(async () => {
setDisplayedCode((prev) => [...prev, { text: "" }]);
await typeCode(block.content, block.delay, (newText) =>
setDisplayedCode((prev) => {
const updatedBlocks = [...prev];
updatedBlocks[idx].text = newText;
return updatedBlocks;
})
);
if (idx === codeBlocks.length - 1) {
// Stop the typing sound when the animation completes
if (typingSoundRef.current) {
typingSoundRef.current.pause();
typingSoundRef.current.currentTime = 0; // Reset for next play
}
stopTimer();
}
}, block.time);
});
};
return (
<div>
<div className="text-white p-2 rounded text-sm text-center">
Elapsed Time: {elapsedTime} seconds / Estimated Total Time:{" "}
{totalEstimatedTime.toFixed(2)} seconds
</div>
<div className="flex border items-center border-gray-800 h-[720px] w-[1280px] mx-auto">
<div className="code-animation-container w-full font-mono p-5 rounded-lg max-w-[1000px] mx-auto text-center text-xl">
{started ? (
displayedCode.map((block, idx) => (
<SyntaxHighlighter
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={idx}
language="javascript"
style={darcula}
className="custom-syntax-highlighter"
>
{block.text}
</SyntaxHighlighter>
))
) : (
<button
type="button"
onClick={startAnimation}
className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
>
Start Animation
</button>
)}
<style jsx>{`
:global(.custom-syntax-highlighter pre),
:global(.custom-syntax-highlighter code),
:global(.custom-syntax-highlighter) {
background: none !important;
}
`}</style>
</div>
</div>
</div>
);
}