The ship has a new instrument panel: a typing test. It’s small, fast, and politely judgmental — like a lighthouse that also tracks your WPM.
Mission: build a clean typing test with zero libraries, clear state, and timing that won’t drift like a drunk compass.
If you hear splashing, that’s just me throwing edge cases overboard.
A typing test looks simple until you decide it should be correct. “Correct” doesn’t mean fancy UI or a leaderboard. It means:
Design choice: I didn’t do a fixed 60-second run. Some tests do that. Mine ends on completion.
Why? Because I’m measuring how you navigate the full passage, not how you panic for a minute.
The entire test hangs off a small handful of state variables. If you can’t name your state, you can’t steer your ship.
let targetText = "";
let started = false;
let elapsedSeconds = 0;
let timer = null;
let startTime = null;
That’s it. Not a thousand flags. Not a state machine diagram painted on the sails. Just enough to answer:
Here’s the trap: if you “tick” by incrementing a counter once per second, your timer will drift. Tabs sleep. Browsers throttle. Computers do computer things. The sea gets choppy.
Instead, I anchor time to reality:
elapsedSeconds = Math.floor((Date.now() - startTime) / 1000);
If the browser delays a tick, I don’t care. I recalculate from the real clock and keep the log honest. Your time is your time — not a hopeful approximation.
Typing tests love inflating numbers. I don’t. My WPM is based on correct characters, not “total characters including the ones you faceplanted into the reef.”
const minutes = elapsedSeconds / 60;
const wpm = minutes > 0 ? Math.round((correct / 5) / minutes) : 0;
The / 5 is the classic “five characters equals a word” convention.
It’s imperfect, but it’s stable across punctuation-heavy prompts and it rewards accuracy.
WPM alone is a speedometer. Accuracy is the hull integrity meter. If you’re “fast” while typing nonsense, congratulations — you’re a torpedo.
const accuracy = totalTyped > 0
? Math.round((correct / totalTyped) * 100)
: 100;
The prompt isn’t a static paragraph. It’s a character-by-character display where each character becomes:
And I draw a cursor by adding a class to the next expected character. No canvas. No magic. Just spans and CSS.
for (let i = 0; i < targetText.length; i++) {
const span = document.createElement("span");
const char = targetText[i];
if (i < typed.length) {
span.className = typed[i] === char ? "correct" : "incorrect";
} else {
span.className = "pending";
}
if (i === typed.length) {
span.className += " cursor";
}
span.textContent = char;
textEl.appendChild(span);
}
This is the kind of tiny UI trick that feels way fancier than it is. Like a sea shanty with three chords.
The test starts on the first input event. Not on page load. Not on focus. Not on vibes. When you type, the clock starts.
function startTest() {
if (started) return;
started = true;
startTime = Date.now();
timer = setInterval(tick, 1000);
}
inputEl.addEventListener("input", () => {
if (!started) startTest();
updateStats();
});
The end condition is intentionally simple: once you’ve typed as many characters as the prompt contains, the run stops and input locks.
if (typed.length >= targetText.length) {
endTest();
}
No “submit” button. No ambiguity. Finish the passage, dock the ship.
The test works. It’s snappy. It’s consistent with the site’s dark-ocean aesthetic. But I’m not done poking it with a harpoon.
For now: the instrument is installed, the needle moves, and the sea is wide. If you beat my WPM, keep it to yourself. I’m fragile. (I’m not.)
Go test your mettle: tentacodesplayground.com/typing-test.html