DEV Community

Cover image for I built a typing speed test in one HTML file — no dependencies, free to host forever
sneh1117
sneh1117

Posted on

I built a typing speed test in one HTML file — no dependencies, free to host forever

I wanted a typing speed test I actually liked using. Every existing one either looks like it was designed in 2009, shoves ads in your face, or wants an account just to save your scores.

So I built my own. One file. Zero dependencies. Free to host forever on GitHub Pages.

Live: sneh1117.github.io/Typing_test
Repo: github.com/sneh1117/Typing_test


What it does

No fluff, just the features that matter:

  • Live WPM and accuracy updating on every keystroke — not just at the end
  • Color-coded characters as you type — green correct, red wrong, instantly
  • Three time modes — 30s, 60s, 2 minutes
  • WPM-over-time bar chart on the results screen so you can see where you peak
  • 20 shuffled paragraphs so rounds never feel identical
  • Tab or Esc to restart without touching the mouse
  • Dark theme that doesn't hurt your eyes after 30 rounds

No signup. No ads. No framework. Just a file.


The constraint: one HTML file, no external requests

Every typing test I found either called out to a CDN, loaded Google Fonts, or had some analytics script running in the background. Stuff that can go down, slow you down, or just feel gross.

My rule: the whole thing ships in one index.html with zero network requests at runtime. Open it locally, host it on GitHub Pages, doesn't matter — it just works.

That ended up being about 250 lines of HTML, CSS, and vanilla JS. Readable in one sitting.


How the core stuff works

Rendering characters as spans

Rather than a textarea with an overlay (messy), I render each character as its own <span> when the paragraph loads:

w.split('').forEach((ch, ci) => {
  const span = document.createElement('span');
  span.className = 'char';
  span.textContent = ch;
  wordEl.appendChild(span);
});
Enter fullscreen mode Exit fullscreen mode

As you type, I just toggle .correct or .wrong on the right span:

if (typedChar === correctChar) {
  correctChars++;
  charEl.classList.add('correct');
} else {
  wrongChars++;
  charEl.classList.add('wrong');
}
Enter fullscreen mode Exit fullscreen mode

The cursor is a CSS ::before pseudo-element on the .current span — a 2px blinking line, pure CSS, no JS involved.

The hidden input trick

A clean pattern for custom typing UIs: hide a real <input> off-screen and let it capture all the keystrokes. The visible display is plain DOM — no contenteditable, no cursed div editing.

<input id="hidden-input" type="text"
  autocomplete="off" autocorrect="off"
  autocapitalize="off" spellcheck="false">
Enter fullscreen mode Exit fullscreen mode
input.addEventListener('input', function(e) {
  if (!started) { started = true; startTimer(); }
  // handle character...
});
Enter fullscreen mode Exit fullscreen mode

This also means mobile keyboards work correctly and you can explicitly kill autocorrect.

WPM formula

Standard gross WPM — the same one Monkeytype and TypeRacer use:

function calcWpm() {
  const elapsed = (totalTime - timeLeft) || 1;
  return Math.round((correctChars / 5) / (elapsed / 60));
}
Enter fullscreen mode Exit fullscreen mode

5 characters = 1 word, industry standard. Gross WPM counts all correct characters, not just completed words. Live-updates every second via the timer interval.

WPM over time — no chart library needed

Every 5 seconds I push a snapshot:

if (elapsed > 0 && elapsed % 5 === 0) {
  wpmHistory.push({ t: elapsed, wpm: calcWpm() });
}
Enter fullscreen mode Exit fullscreen mode

On the results screen, the chart is just divs with proportional heights:

const maxWpm = Math.max(...wpmHistory.map(p => p.wpm), 1);
wpmHistory.forEach(p => {
  const bar = document.createElement('div');
  bar.style.height = Math.round((p.wpm / maxWpm) * 56) + 'px';
  chart.appendChild(bar);
});
Enter fullscreen mode Exit fullscreen mode

No canvas. No library. 10 lines.


Deploy it yourself

  1. Create a GitHub repo
  2. Drop in index.html
  3. Settings → Pages → Source → main / root
  4. Live at sneh1117.github.io/Typing_test

No build step. No CI. No Dockerfile. A file going up.


What I left out on purpose

A few things I skipped to keep it one file, but would be straightforward to add:

  • Personal best tracking via localStorage — maybe 15 lines
  • Custom word lists — a textarea to paste your own text
  • Sound feedback — correct click, wrong thud, all doable with Web Audio API and zero audio files
  • Smooth cursor animation — interpolate position instead of jumping

PRs open if any of those appeal to you.


The bigger point

This is part of a pattern I've been enjoying: high-reward, low-effort websites you can actually ship in an afternoon and forget about forever.

A typing test. An ambient noise mixer. An "is it Friday?" page. Tiny projects that live on GitHub Pages for free, never need maintenance, and are genuinely useful to someone.

No AWS bill. No framework upgrade treadmill. No database to babysit.

Sometimes the best side project is the one that fits in one file.


Source and full code: github.com/sneh117/Typing_test

Top comments (0)