Fun & Developer Toys

MINIMAL WPM SPEED TYPER

Test your typing speed and accuracy. Mechanical key click sounds included. Click box and start typing!

0 WPM
100% Accuracy
30s Time Remaining
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

const SAMPLE_TEXT = "the quick brown fox jumps over the lazy dog quantum mechanics describes nature at the scale of atoms and subatomic particles speed typing improves muscle memory and developer productivity";

    let words = [];
    let charIndex = 0;
    let errors = 0;
    let timer = null;
    let timeLeft = 30;
    let isRunning = false;

    const wordsWrap = document.getElementById('words-wrap');
    const input = document.getElementById('type-input');
    const wpmEl = document.getElementById('stat-wpm');
    const accEl = document.getElementById('stat-acc');
    const timeEl = document.getElementById('stat-time');
    const soundChk = document.getElementById('chk-sound');

    let audioCtx = null;
    function playKeyClick() {
      if (!soundChk.checked) return;
      if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      if (audioCtx.state === 'suspended') audioCtx.resume();
      const now = audioCtx.currentTime;
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(600 + Math.random() * 200, now);
      gain.gain.setValueAtTime(0.08, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.03);
      osc.connect(gain); gain.connect(audioCtx.destination);
      osc.start(now); osc.stop(now + 0.03);
    }

    function initTest() {
      clearInterval(timer);
      timeLeft = 30;
      charIndex = 0;
      errors = 0;
      isRunning = false;

      wpmEl.textContent = '0';
      accEl.textContent = '100%';
      timeEl.textContent = '30s';
      input.value = '';

      wordsWrap.innerHTML = '';
      SAMPLE_TEXT.split('').forEach((ch, idx) => {
        const span = document.createElement('span');
        span.className = 'char' + (idx === 0 ? ' current' : '');
        span.textContent = ch;
        wordsWrap.appendChild(span);
      });
    }

    function startTimer() {
      isRunning = true;
      timer = setInterval(() => {
        timeLeft--;
        timeEl.textContent = timeLeft + 's';
        if (timeLeft <= 0) {
          clearInterval(timer);
          isRunning = false;
          input.blur();
        }
      }, 1000);
    }

    document.getElementById('typer-card').addEventListener('click', () => input.focus());

    input.addEventListener('input', e => {
      if (!isRunning && timeLeft > 0) startTimer();
      playKeyClick();

      const chars = wordsWrap.querySelectorAll('.char');
      const val = input.value;
      const currentCh = val[val.length - 1];

      if (charIndex < chars.length && timeLeft > 0) {
        chars.forEach(c => c.classList.remove('current'));

        if (currentCh === chars[charIndex].textContent) {
          chars[charIndex].classList.add('correct');
        } else {
          chars[charIndex].classList.add('wrong');
          errors++;
        }

        charIndex++;
        if (charIndex < chars.length) chars[charIndex].classList.add('current');

        // WPM & Acc
        const elapsed = (30 - timeLeft) / 60 || 0.01;
        const wpm = Math.round((charIndex / 5) / elapsed);
        const accuracy = Math.max(0, Math.round(((charIndex - errors) / charIndex) * 100)) || 100;

        wpmEl.textContent = isNaN(wpm) ? 0 : wpm;
        accEl.textContent = accuracy + '%';
      }
    });

    document.getElementById('btn-restart').addEventListener('click', initTest);

    initTest();