There’s no better way to level up your front-end skills than by building something you can actually play with. In this guide, you’ll create a typing speed test game in JavaScript that puts your knowledge of DOM manipulation, events, and timers to work. Instead of a throwaway demo, you’ll end up with a polished game that measures words per minute, accuracy, and elapsed time in a clean interface.
Why a Typing Game Is the Perfect JavaScript Practice Project
Typing games look simple, but they require a surprising amount of care. You need a responsive input area, live visual feedback, accurate timing, and a way to detect errors. That makes this mini-project ideal for practicing event-driven programming and state management. The game you build today will have three core features:
- A prompt phrase that appears on the screen.
- An input area that tracks every typed character.
- A live timer that drives the score and the game-over logic.
By the time you’re done, you’ll have a reusable pattern for handling user input, updating the DOM without flicker, and avoiding the timer bugs that trip up many beginners.
Setting Up the HTML Shell for the Game
Start with a minimal HTML structure. You need a place to show the prompt, a textarea for typing, a stats area, and a restart button. This gives you a clear skeleton before any JavaScript gets involved.
<div id="typing-app">
<h2>Type the sentence below</h2>
<p id="prompt"></p>
<textarea id="input-area" placeholder="Click here and start typing..."></textarea>
<div id="results">
<span>Time: <strong id="time">0s</strong></span>
<span>WPM: <strong id="wpm">0</strong></span>
<span>Accuracy: <strong id="accuracy">100%</strong></span>
</div>
<button id="restart-btn">Restart</button>
</div>
The textarea is the heart of the game. It lets users type naturally while also giving you a value property that makes it easy to read the entire typed string at once.
Writing the JavaScript: Selectors and Game State
Before writing any logic, grab all the elements you’ll need and store the game data in a single object. Keeping UI elements and state separate makes the reset flow much easier to manage.
const promptElement = document.getElementById('prompt');
const inputArea = document.getElementById('input-area');
const timeElement = document.getElementById('time');
const wpmElement = document.getElementById('wpm');
const accuracyElement = document.getElementById('accuracy');
const restartButton = document.getElementById('restart-btn');
const state = {
quote: 'The quick brown fox jumps over the lazy dog',
started: false,
timerId: null,
startTime: 0,
elapsed: 0,
correctChars: 0,
totalTyped: 0,
isActive: true
};
This state object holds everything that changes during the game: whether the timer has started, the current elapsed seconds, character totals, and whether the game is still active. This central approach prevents scattered variables and makes the restart button trivial to implement.
Building a Reliable Timer with Date.now()
The first big challenge is measuring time. Many beginner tutorials use setInterval to increment a counter, but browser timers can be delayed or throttled in background tabs. A better approach is to store the exact start time with Date.now(), then let setInterval only handle updating the displayed time.
function startTimer() {
state.started = true;
state.startTime = Date.now();
state.timerId = setInterval(updateTimer, 50);
}
function updateTimer() {
if (!state.isActive) return;
state.elapsed = Math.floor((Date.now() - state.startTime) / 1000);
timeElement.textContent = state.elapsed + 's';
}
The 50-millisecond interval feels smooth without forcing the browser to repaint too often. Because the actual elapsed time is always computed from real timestamps, your timer stays accurate even if a few interval callbacks run late.
Handling Input with the Right Events
Next, connect the textarea to the game logic. Use the input event instead of keydown for reading the typed content. Why? Because the input event fires whenever the textarea’s value changes, including when users paste text, use browser autocomplete, or rely on an on-screen keyboard.
inputArea.addEventListener('input', () => {
if (!state.started) startTimer();
if (!state.isActive) return;
const typed = inputArea.value;
const target = state.quote;
state.totalTyped = typed.length;
state.correctChars = 0;
for (let i = 0; i < target.length; i++) {
if (i >= typed.length) break;
if (typed[i] === target[i]) {
state.correctChars++;
}
}
updateStats();
updatePromptDisplay(typed);
if (typed.length >= target.length) endGame();
});
This comparison loop checks each typed character against the matching character in the prompt. It gives you the exact number of correct characters, which is all you need for both accuracy and a smart WPM calculation.
Updating the DOM with Visual Feedback
To make the game feel responsive, highlight each character in the prompt as the user types. A straightforward way to do this is to rebuild the prompt’s HTML with span elements that carry a class name.
function updatePromptDisplay(typed) {
const target = state.quote;
let html = '';
for (let i = 0; i < target.length; i++) {
if (i < typed.length) {
const cls = typed[i] === target[i] ? 'correct' : 'error';
html += `<span class="${cls}">${target[i]}</span>`;
} else {
html += `<span>${target[i]}</span>`;
}
}
promptElement.innerHTML = html;
}
Adding a little CSS makes the feedback obvious:
.correct { color: #2e7d32; }
.error { color: #c62828; text-decoration: underline; }
This approach is simple and performant for a normal sentence-length prompt. You don’t need to manage dozens of individual nodes; just update the prompt’s inner HTML on every input event.
Measuring WPM and Accuracy in Real Time
Now that the timer is running and the input is tracked, it’s time to calculate the two most important stats: words per minute and accuracy. In typing tests, a “word” is standardized as five characters, so you can compute WPM by dividing the number of correctly typed characters by five, then dividing again by the elapsed time in minutes.
function updateStats() {
const minutes = state.elapsed / 60;
const typedWords = state.correctChars / 5;
const wpm = minutes > 0 ? Math.round(typedWords / minutes) : 0;
const accuracy = state.totalTyped === 0 ? 100 : Math.round((state.correctChars / state.totalTyped) * 100);
wpmElement.textContent = wpm;
accuracyElement.textContent = accuracy + '%';
}
If the user has been typing for less than a second, the game shows 0 WPM instead of a misleading huge number. Accuracy is simply the percentage of correctly typed characters among all characters typed so far.
Ending the Game and Avoiding Race Conditions
When the user reaches the end of the prompt, the game should stop immediately. Critically, you must clear the interval and disable the textarea so no further input can change the score.
function endGame() {
if (!state.isActive) return;
state.isActive = false;
clearInterval(state.timerId);
updateStats();
inputArea.disabled = true;
}
function restartGame() {
clearInterval(state.timerId);
state.isActive = true;
state.started = false;
state.elapsed = 0;
state.correctChars = 0;
state.totalTyped = 0;
inputArea.disabled = false;
inputArea.value = '';
timeElement.textContent = '0s';
wpmElement.textContent = '0';
accuracyElement.textContent = '100%';
promptElement.textContent = state.quote;
}
restartButton.addEventListener('click', restartGame);
The restart function is where many race conditions surface. If you don’t clear the previous interval, multiple timers can end up running at once. Always call clearInterval(state.timerId) before reinitializing the game state.
Sharpening the Experience with Small Enhancements
Once the core loop is works, you can polish the game without overcomplicating it. For example, prevent users from pasting large blocks of text into the textarea:
inputArea.addEventListener('keydown', (e) => {
if (e.key === 'Enter') e.preventDefault();
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'v') {
e.preventDefault();
}
});
Blocking the Enter key keeps the prompt on a single line, while blocking paste keeps the test honest. You can also add a hard cutoff such as a 30-second countdown ending the game, or shuffle a small array of quotes each time the restart button is clicked.
Final Thoughts
Building a typing speed test game in JavaScript is a compact but powerful way to practice the three pillars of interactive web apps: reading user input, updating the DOM, and managing time-based logic. With the Date.now() timer pattern, the input event approach, and a clean reset flow, you now have a solid foundation you can extend with multiple quotes, remote API prompts, or a local leaderboard. The more you refine it, the more you’ll learn about the subtle relationship between browser events, state changes, and visual feedback.
