Give every input a meaning

Reflex Lab has four phases: idle, waiting, go, and done. During idle there is no round to measure. During waiting, a tap is too early. During go, a tap records a result. During done, further taps should do nothing. Writing down that small state machine makes the rules more reliable than inferring them from the panel's color.

The shared Start button begins a round. The target panel accepts pointer input and Space or Enter. Text accompanies the color change, so the cue is not communicated by green alone. Before changing the theme, preserve that wording and test keyboard focus on the target.

Start timing at the cue

timer = setTimeout(() => {
  if (!running) return;
  phase = 'go';
  startTime = performance.now();
  target.className = 'reaction go';
  target.textContent = 'GO! Tap now';
  statusEl.textContent = 'Go!';
}, 1500 + Math.random() * 2500);

This is the existing cue callback. The random wait is at least 1500 milliseconds and less than 4000 milliseconds. The measurement starts when the callback runs, rather than when Start was pressed. Starting earlier would mix the waiting period into the result.

performance.now() provides a monotonic time source suitable for measuring elapsed time within a page. The recorded number is the difference between two timestamps, rounded to milliseconds. It still includes browser scheduling, rendering, and input delays; displaying milliseconds does not make this a laboratory measurement.

Treat early taps as a finished round

If a player taps while waiting, the template clears the pending timeout, marks the phase done, shows an early message, and finishes the round. The timeout must be cancelled. Otherwise the old green cue could appear after the early-tap message, suggesting that the failed round was still active.

A restart calls reset, which also clears the timeout before returning to idle. This covers the case where someone restarts during the random waiting period. The check inside the callback is an additional guard; it does not replace cleaning up the timer that belongs to the previous round.

Keep one press to one attempt

The target uses pointerdown so a tap is measured on press rather than release. Its keyboard handler rejects repeated keydown events with !e.repeat. Holding Space should not create a stream of attempts. The phase check then ensures only the first valid response after the cue produces a score.

If you add a click handler as well, account for the click that follows a pointer press. Connecting both blindly can process the same physical action twice. Keep one input path per action, or make the central state transition safely reject duplicates.

Run a useful set of trials

Test an early tap, a successful response, repeated taps after the result, and a restart while waiting. Also switch to another tab during the wait. Browser scheduling can change while a page is in the background. For a polished extension, cancel or invalidate that trial on visibilitychange rather than presenting it as comparable to an uninterrupted attempt.

A five-trial summary is a good next feature. Store only successful results, show how many attempts remain, and calculate a median after five. Do not count early taps as zero milliseconds, because zero would make the summary look faster. If you compare devices, keep the input method and conditions consistent and label the result as a casual game score.

Try changing the wait range to 2000–5000 milliseconds. This affects anticipation without changing the timing calculation. Keep the cue unpredictable enough that repeatedly guessing does not become the best strategy.

Put this lesson into practice: download Reflex Lab or play the original before making your changes.

Keep building