JavaScript Notes
Build a fully functional stopwatch from scratch using JavaScript setInterval, DOM manipulation, and event handling. Includes start, stop, reset, and lap features with complete source code.
Introduction
A stopwatch is one of the best beginner projects to master setInterval, clearInterval, DOM manipulation, and state management in JavaScript. In this tutorial, you will build a fully functional digital stopwatch that can start, stop, reset, and record lap times.
What you will learn:
- How
setIntervalandclearIntervalwork together to create a ticking clock - Managing application state (running, paused, elapsed time)
- Formatting milliseconds into
MM:SS:MSdisplay format - Recording and displaying lap times dynamically
- Clean event handler patterns for UI controls
Prerequisites: Basic understanding of HTML, CSS, and JavaScript variables, functions, and DOM selection (getElementById, querySelector).
HTML Structure
We need a simple layout: a time display area, control buttons, and a lap list container.
CSS Styling
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, sans-serif;
background: linear-gradient(135deg, #1a1a2e, #16213e);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.stopwatch-container {
background: #0f3460;
border-radius: 20px;
padding: 40px;
text-align: center;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
min-width: 380px;
}
.title {
color: #e94560;
margin-bottom: 20px;
font-size: 1.5rem;
}
.time-display {
font-size: 3.5rem;
font-family: 'Courier New', monospace;
color: #ffffff;
background: #1a1a2e;
padding: 20px 30px;
border-radius: 12px;
margin-bottom: 30px;
letter-spacing: 4px;
}
.controls {
display: flex;
gap: 12px;
justify-content: center;
margin-bottom: 30px;
}
.btn {
padding: 12px 24px;
font-size: 1rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: transform 0.1s, opacity 0.2s;
}
.btn:hover { transform: scale(1.05); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-start { background: #4ecca3; color: #1a1a2e; }
.btn-reset { background: #e94560; color: #fff; }
.btn-lap { background: #f9a825; color: #1a1a2e; }
.laps-container { max-height: 200px; overflow-y: auto; }
.laps-title {
color: #4ecca3;
margin-bottom: 10px;
font-size: 0.9rem;
}
.laps-list {
list-style: none;
text-align: left;
}
.laps-list li {
color: #ccc;
padding: 6px 12px;
border-bottom: 1px solid #1a1a2e;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
display: flex;
justify-content: space-between;
}
.laps-list li span.lap-number { color: #f9a825; }JavaScript — Step by Step
Step 1: Timer State
Every stopwatch needs state to track whether it is running, when it started, and how much time has passed.
// ===== STATE =====
const state = {
isRunning: false,
startTime: 0,
elapsedTime: 0, // total milliseconds elapsed
intervalId: null, // reference to setInterval for cleanup
laps: [] // array of lap timestamps
};Why elapsedTime instead of counting intervals? If you count count++ every 10ms, drift accumulates. Instead, we store the real start timestamp and subtract it from Date.now() each tick. This gives accurate elapsed time regardless of interval drift.
Step 2: DOM References
Cache DOM elements at the top to avoid repeated lookups inside the interval loop.
// ===== DOM REFERENCES =====
const display = document.getElementById('display');
const startStopBtn = document.getElementById('startStopBtn');
const resetBtn = document.getElementById('resetBtn');
const lapBtn = document.getElementById('lapBtn');
const lapsList = document.getElementById('lapsList');
const lapsTitle = document.getElementById('lapsTitle');Step 3: Formatting Time Display
We need to convert raw milliseconds into a human-readable MM:SS:ms format.
// ===== FORMAT TIME =====
function formatTime(ms) {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const centiseconds = Math.floor((ms % 1000) / 10);
return (
String(minutes).padStart(2, '0') + ':' +
String(seconds).padStart(2, '0') + ':' +
String(centiseconds).padStart(2, '0')
);
}formatTime(0) → "00:00:00" formatTime(5000) → "00:05:00" formatTime(61230) → "01:01:23" formatTime(3599990) → "59:59:99"
Key method: String.padStart(2, '0') ensures single-digit numbers display with a leading zero (e.g., 5 becomes "05").
Step 4: Update Display Function
This function is called every 10ms by the interval. It calculates elapsed time and renders it.
// ===== UPDATE DISPLAY =====
function updateDisplay() {
const currentTime = Date.now();
const totalElapsed = state.elapsedTime + (currentTime - state.startTime);
display.textContent = formatTime(totalElapsed);
}Why state.elapsedTime + (currentTime - state.startTime)? When the user pauses and resumes, state.elapsedTime stores previously accumulated time. The (currentTime - state.startTime) portion is only the time since the last resume.
Step 5: Start and Stop Logic
The start/stop button toggles behavior based on current state.
// ===== START / STOP =====
function handleStartStop() {
if (state.isRunning) {
// STOP: pause the timer
clearInterval(state.intervalId);
state.elapsedTime += Date.now() - state.startTime;
state.isRunning = false;
state.intervalId = null;
// Update UI
startStopBtn.textContent = 'Start';
startStopBtn.classList.replace('btn-stop', 'btn-start');
lapBtn.disabled = true;
} else {
// START: begin or resume the timer
state.startTime = Date.now();
state.isRunning = true;
state.intervalId = setInterval(updateDisplay, 10);
// Update UI
startStopBtn.textContent = 'Stop';
startStopBtn.classList.replace('btn-start', 'btn-stop');
lapBtn.disabled = false;
}
}Click Start → timer begins ticking: 00:00:01, 00:00:02 ... Click Stop → timer freezes at current value Click Start → timer resumes from where it stopped
Critical: Always call clearInterval(state.intervalId) before discarding the reference. Forgetting this causes memory leaks and phantom intervals that keep running in the background.
Step 6: Reset Logic
Reset clears all state and returns the display to 00:00:00.
// ===== RESET =====
function handleReset() {
// Stop any running interval
clearInterval(state.intervalId);
// Reset all state
state.isRunning = false;
state.startTime = 0;
state.elapsedTime = 0;
state.intervalId = null;
state.laps = [];
// Reset UI
display.textContent = '00:00:00';
startStopBtn.textContent = 'Start';
startStopBtn.classList.replace('btn-stop', 'btn-start');
lapBtn.disabled = true;
lapsList.innerHTML = '';
lapsTitle.textContent = '';
}Step 7: Lap Feature
The lap button records the current elapsed time and displays it in a list.
Click Lap at 00:05:32 → displays "Lap 1 00:05:32" Click Lap at 00:12:87 → displays "Lap 2 00:12:87" (newest on top) Click Lap at 00:20:15 → displays "Lap 3 00:20:15" (newest on top)
Step 8: Wire Up Event Listeners
// ===== EVENT LISTENERS =====
startStopBtn.addEventListener('click', handleStartStop);
resetBtn.addEventListener('click', handleReset);
lapBtn.addEventListener('click', handleLap);Complete Code (script.js)
Here is the full JavaScript file assembled together:
Browser Console Test: > formatTime(0) // "00:00:00" > formatTime(65430) // "01:05:43" > Click Start → display starts ticking > Click Lap → "Lap 1 00:03:25" appears > Click Stop → timer pauses > Click Start → timer resumes from paused value > Click Reset → everything returns to 00:00:00
Key Takeaways
- Use
Date.now()for accurate timing — never rely on counting interval ticks. Intervals can drift due to browser throttling, garbage collection, or tab inactivity.
- Always store
intervalId— without a reference to the interval, you cannot stop it. Leaked intervals cause performance issues and unexpected behavior.
- Separate state from UI — the
stateobject is the single source of truth. Display functions only read from state; they never create their own tracking variables.
clearIntervalis idempotent — calling it withnullor an invalid ID does nothing and won't throw an error. This makes reset logic safe to call anytime.
padStartfor display formatting —String(n).padStart(2, '0')is the cleanest way to ensure two-digit display without manual if-else chains.
- Pause/resume pattern — on pause, accumulate elapsed time into
state.elapsedTime. On resume, set a freshstartTime. Total elapsed = stored + current session.
- Disable buttons contextually — the Lap button is disabled when the timer is not running. This prevents invalid state entries and improves UX.
- Render in reverse for latest-first — looping backwards through the laps array shows the most recent lap at the top, which is the standard UX pattern for stopwatch apps.
Frequently Asked Questions
Q1: Why use setInterval(fn, 10) instead of setInterval(fn, 1000)?
A stopwatch needs to display centiseconds (hundredths of a second) for precision. A 1000ms interval would only update once per second, making the display appear frozen between updates. The 10ms interval provides smooth visual updates. Note that actual precision depends on the browser — most browsers have a minimum interval of ~4ms, and background tabs may throttle to 1000ms. The Date.now() calculation ensures the displayed time is always accurate regardless of actual interval timing.
Q2: What is the difference between a stopwatch and a countdown timer?
A stopwatch counts upward from zero and measures elapsed time. A countdown timer starts from a target time and counts downward to zero. The core mechanism (setInterval + time calculation) is identical — the difference is the math: stopwatch uses currentTime - startTime while countdown uses targetTime - currentTime. Both need clearInterval to stop.
Q3: How can I make this stopwatch persist across page reloads?
Use localStorage to save state.elapsedTime and a flag indicating whether the timer was running. On page load, read these values and restore the state:
1. Timer at 00:05:30, page refreshed 2. Page loads → reads localStorage → shows 00:05:32 (added reload gap) 3. Timer continues running automatically
This pattern accounts for time passing while the page was closed by storing the timestamp of the last save.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for JavaScript Stopwatch Project — setInterval & DOM Tutorial 2026.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this JavaScript Master Course topic.
Search Terms
javascript-complete-guide, javascript master course, javascript, complete, guide, mini, projects, stopwatch
Related JavaScript Master Course Topics