October 31, 2024
O. Wolfson
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.
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.
javascriptconst 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:
typescript"use client";
import { useState, useRef } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism";
// Type definition for a code block
interface CodeBlock {
time: number;
content: string;
delay: number;
}
// Code block configuration
const codeBlocks: CodeBlock[] = [
{
time: 1000,
content: `
import { createApiClient } from "./apiClient";
import { getSession } from "./sessionHandler";
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 ( index = ; index < content.; index++) {
(content.(, index + ));
( (resolve, delay));
}
}
(): {
totalCharacters = codeBlocks.(
sum + block..,
);
totalDelay = codeBlocks.( sum + block., );
typingTime = totalCharacters * codeBlocks[].;
(totalDelay + typingTime) / ;
}
() {
[displayedCode, setDisplayedCode] = useState<{ : }[]>([]);
[started, setStarted] = useState<>();
[elapsedTime, setElapsedTime] = useState<>();
timerRef = useRef<. | >();
totalEstimatedTime = (codeBlocks);
= () => {
timerRef. = (
( prev + ),
);
};
= () => {
(timerRef.) {
(timerRef.);
}
};
= () => {
();
();
();
codeBlocks.( {
( () => {
( [...prev, { : }]);
(block., block.,
( {
updatedBlocks = [...prev];
updatedBlocks[idx]. = newText;
updatedBlocks;
})
);
(idx === codeBlocks. - ) ();
}, block.);
});
};
(
);
}
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.
typescript"use client";
import { useState, useRef, useEffect } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism";
// Type definition for a code block
interface CodeBlock {
time: number;
content: string;
delay: number;
}
// Code block configuration
const codeBlocks: CodeBlock[] = [
{
time: 1000,
content: `
import { createApiClient } from "./apiClient";
import { getSession } from "./sessionHandler";
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 ( index = ; index < content.; index++) {
(content.(, index + ));
( (resolve, delay));
}
}
(): {
totalCharacters = codeBlocks.(
sum + block..,
);
totalDelay = codeBlocks.( sum + block., );
typingTime = totalCharacters * codeBlocks[].;
(totalDelay + typingTime) / ;
}
() {
[displayedCode, setDisplayedCode] = useState<{ : }[]>([]);
[started, setStarted] = useState<>();
[elapsedTime, setElapsedTime] = useState<>();
timerRef = useRef<. | >();
totalEstimatedTime = (codeBlocks);
typingSoundRef = useRef< | >();
( {
typingSoundRef. = ();
}, []);
= () => {
timerRef. = (
( prev + ),
);
};
= () => {
(timerRef.) {
(timerRef.);
}
};
= () => {
();
();
();
(typingSoundRef.) {
typingSoundRef.. = ;
typingSoundRef..().( {
.(, err);
});
}
codeBlocks.( {
( () => {
( [...prev, { : }]);
(block., block.,
( {
updatedBlocks = [...prev];
updatedBlocks[idx]. = newText;
updatedBlocks;
})
);
(idx === codeBlocks. - ) {
(typingSoundRef.) {
typingSoundRef..();
typingSoundRef.. = ;
}
();
}
}, block.);
});
};
(