A typing speed test that measures your WPM and accuracy in real time — with character-level highlighting, a 60-second countdown, and a results screen.
The test shows a passage to type, an input field, live stats, and a timer.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Typing Speed Test</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<?php require_once __DIR__ . '/../promo_banner.php'; ?>
<div class="app">
<h1>⌨️ Typing Speed Test</h1>
<div class="stats-bar">
<div class="stat"><div class="stat-val" id="wpm">0</div><div class="stat-lbl">WPM</div></div>
<div class="stat"><div class="stat-val" id="accuracy">100</div><div class="stat-lbl">% Accuracy</div></div>
<div class="stat"><div class="stat-val" id="timer">60</div><div class="stat-lbl">Seconds</div></div>
</div>
<div class="passage" id="passage"></div>
<textarea id="inputBox" placeholder="Click here and start typing..." rows="4" disabled></textarea>
<button id="startBtn" onclick="startTest()">Start Test</button>
<div class="result hidden" id="result"></div>
</div>
<script src="app.js"></script>
</body>
</html>
Wrap each character in a span so we can colour correct/incorrect letters in real time.
// app.js
const PASSAGES = [
"The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs.",
"Success is not final, failure is not fatal: it is the courage to continue that counts.",
"Python is a versatile programming language used for web development, data science, and automation.",
"In the beginning was the Word, and the Word was with God, and the Word was God.",
"To be or not to be, that is the question. Whether tis nobler in the mind to suffer the slings and arrows.",
];
let passage, typed = '', startTime, countdown, timeLeft = 60, active = false;
function renderPassage() {
passage = PASSAGES[Math.floor(Math.random() * PASSAGES.length)];
const el = document.getElementById('passage');
el.innerHTML = passage.split('').map((ch, i) =>
`<span id="c${i}">${ch === ' ' ? ' ' : ch}</span>`
).join('');
}
function colourChars(input) {
for (let i = 0; i < passage.length; i++) {
const span = document.getElementById(`c${i}`);
if (i < input.length) {
span.className = input[i] === passage[i] ? 'correct' : 'wrong';
} else if (i === input.length) {
span.className = 'cursor';
} else {
span.className = '';
}
}
}
Start the 60-second timer on first keypress and calculate WPM every second.
function startTest() {
renderPassage();
const box = document.getElementById('inputBox');
box.value = ''; box.disabled = false; box.focus();
timeLeft = 60; active = false;
document.getElementById('result').classList.add('hidden');
document.getElementById('startBtn').textContent = 'Restart';
box.oninput = () => {
if (!active) {
active = true;
startTime = Date.now();
countdown = setInterval(tick, 1000);
}
typed = box.value;
colourChars(typed);
updateStats();
if (typed === passage) endTest();
};
}
function tick() {
timeLeft--;
document.getElementById('timer').textContent = timeLeft;
updateStats();
if (timeLeft <= 0) endTest();
}
function updateStats() {
const mins = (Date.now() - startTime) / 60000;
const words = typed.trim().split(/\s+/).filter(Boolean).length;
const wpm = mins > 0 ? Math.round(words / mins) : 0;
let correct = 0;
for (let i = 0; i < typed.length; i++) {
if (typed[i] === passage[i]) correct++;
}
const acc = typed.length > 0 ? Math.round(correct / typed.length * 100) : 100;
document.getElementById('wpm').textContent = wpm;
document.getElementById('accuracy').textContent = acc;
}
function endTest() {
clearInterval(countdown);
document.getElementById('inputBox').disabled = true;
const wpm = document.getElementById('wpm').textContent;
const acc = document.getElementById('accuracy').textContent;
const grade = wpm >= 80 ? '🏆 Expert' : wpm >= 60 ? '⭐ Advanced' : wpm >= 40 ? '👍 Average' : '📚 Keep Practising';
const result = document.getElementById('result');
result.innerHTML = `<b>${grade}</b> — ${wpm} WPM · ${acc}% accuracy`;
result.classList.remove('hidden');
}
A fully functional typing test in under 120 lines. The per-character colouring technique is the same approach used by Monkeytype and TypeRacer.
Spotted a bug, broken code, or something that doesn't look right? Tell us what's off and we'll fix it.