Learn how to easily add a timer and stopwatch to your web app with this step-by-step guide. Simple, fast, and effective!

Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
Why Timing Matters in Modern Web Applications
Time-tracking features like timers and stopwatches aren't just nice-to-haves—they're essential components that can significantly enhance user experience in many applications. From workout trackers and cooking apps to project management tools and online exams, these timing elements help users measure, track, and manage their time effectively.
Let's start with a simple countdown timer implementation using vanilla JavaScript:
class Timer {
constructor(durationInSeconds, onTick, onComplete) {
this.duration = durationInSeconds;
this.remaining = durationInSeconds;
this.onTick = onTick || function() {};
this.onComplete = onComplete || function() {};
this.timerId = null;
this.startTime = null;
this.isPaused = false;
}
start() {
if (this.timerId !== null) return; // Already running
this.startTime = Date.now() - ((this.duration - this.remaining) * 1000);
this.timerId = setInterval(() => {
this.remaining = this.duration - Math.floor((Date.now() - this.startTime) / 1000);
if (this.remaining <= 0) {
this.stop();
this.remaining = 0;
this.onComplete();
} else {
this.onTick(this.remaining);
}
}, 100); // Update more frequently than once per second for smoother display
}
pause() {
if (this.timerId === null) return; // Not running
clearInterval(this.timerId);
this.timerId = null;
this.isPaused = true;
}
resume() {
if (!this.isPaused) return;
this.isPaused = false;
this.start();
}
stop() {
if (this.timerId === null) return; // Not running
clearInterval(this.timerId);
this.timerId = null;
this.isPaused = false;
}
reset() {
this.stop();
this.remaining = this.duration;
this.onTick(this.remaining);
}
getTimeRemaining() {
return this.remaining;
}
getFormattedTime() {
const minutes = Math.floor(this.remaining / 60);
const seconds = this.remaining % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
}
Usage example:
// HTML structure
// <div id="timer-display">00:00</div>
// <button id="start-btn">Start</button>
// <button id="pause-btn">Pause</button>
// <button id="reset-btn">Reset</button>
const display = document.getElementById('timer-display');
const startBtn = document.getElementById('start-btn');
const pauseBtn = document.getElementById('pause-btn');
const resetBtn = document.getElementById('reset-btn');
// Create a 2-minute timer
const myTimer = new Timer(120,
// onTick callback
(remaining) => {
display.textContent = myTimer.getFormattedTime();
},
// onComplete callback
() => {
display.textContent = "Time's up!";
// Maybe play a sound or show a notification
}
);
startBtn.addEventListener('click', () => {
if (myTimer.isPaused) {
myTimer.resume();
} else {
myTimer.start();
}
});
pauseBtn.addEventListener('click', () => {
myTimer.pause();
});
resetBtn.addEventListener('click', () => {
myTimer.reset();
});
Now let's implement a stopwatch that counts up instead of down:
class Stopwatch {
constructor(onTick) {
this.startTime = 0;
this.elapsedTime = 0;
this.timerId = null;
this.isRunning = false;
this.onTick = onTick || function() {};
}
start() {
if (this.isRunning) return;
this.isRunning = true;
this.startTime = Date.now() - this.elapsedTime;
this.timerId = setInterval(() => {
this.elapsedTime = Date.now() - this.startTime;
this.onTick(this.elapsedTime);
}, 10); // Update every 10ms for millisecond precision
}
stop() {
if (!this.isRunning) return;
clearInterval(this.timerId);
this.isRunning = false;
}
reset() {
this.elapsedTime = 0;
if (this.isRunning) {
this.startTime = Date.now();
}
this.onTick(this.elapsedTime);
}
lap() {
return this.elapsedTime;
}
getFormattedTime() {
// Convert elapsed time (in ms) to hh:mm:ss.ms format
const ms = Math.floor((this.elapsedTime % 1000) / 10);
const seconds = Math.floor((this.elapsedTime / 1000) % 60);
const minutes = Math.floor((this.elapsedTime / (1000 * 60)) % 60);
const hours = Math.floor(this.elapsedTime / (1000 * 60 * 60));
return `${hours.toString().padStart(2, '0')}:${
minutes.toString().padStart(2, '0')}:${
seconds.toString().padStart(2, '0')}.${
ms.toString().padStart(2, '0')}`;
}
}
Usage example:
// HTML structure
// <div id="stopwatch-display">00:00:00.00</div>
// <button id="start-stop-btn">Start</button>
// <button id="lap-btn">Lap</button>
// <button id="reset-btn">Reset</button>
// <ul id="laps"></ul>
const display = document.getElementById('stopwatch-display');
const startStopBtn = document.getElementById('start-stop-btn');
const lapBtn = document.getElementById('lap-btn');
const resetBtn = document.getElementById('reset-btn');
const lapsList = document.getElementById('laps');
const stopwatch = new Stopwatch((elapsed) => {
display.textContent = stopwatch.getFormattedTime();
});
startStopBtn.addEventListener('click', () => {
if (stopwatch.isRunning) {
stopwatch.stop();
startStopBtn.textContent = 'Start';
} else {
stopwatch.start();
startStopBtn.textContent = 'Stop';
}
});
lapBtn.addEventListener('click', () => {
if (!stopwatch.isRunning) return;
const lapTime = stopwatch.getFormattedTime();
const lapItem = document.createElement('li');
lapItem.textContent = `Lap: ${lapTime}`;
lapsList.appendChild(lapItem);
});
resetBtn.addEventListener('click', () => {
stopwatch.stop();
stopwatch.reset();
startStopBtn.textContent = 'Start';
// Clear laps
lapsList.innerHTML = '';
});
For more complex applications, consider leveraging specialized timing libraries:
Using Moment.js with a custom timer implementation:
// First install moment.js: npm install moment
import moment from 'moment';
class EnhancedTimer {
constructor(durationInSeconds, options = {}) {
this.duration = moment.duration(durationInSeconds, 'seconds');
this.remaining = this.duration.clone();
this.callbacks = {
onTick: options.onTick || function() {},
onComplete: options.onComplete || function() {},
onStart: options.onStart || function() {},
onPause: options.onPause || function() {}
};
this.timerId = null;
this.startTime = null;
this.endTime = null;
this.state = 'idle'; // idle, running, paused, completed
}
start() {
if (this.state === 'running') return;
this.state = 'running';
this.startTime = moment();
this.endTime = moment().add(this.remaining);
this.callbacks.onStart();
this.timerId = setInterval(() => {
const now = moment();
this.remaining = moment.duration(this.endTime.diff(now));
if (this.remaining.asMilliseconds() <= 0) {
this.remaining = moment.duration(0);
this.stop();
this.state = 'completed';
this.callbacks.onComplete();
} else {
this.callbacks.onTick(this.remaining);
}
}, 100);
}
pause() {
if (this.state !== 'running') return;
clearInterval(this.timerId);
this.state = 'paused';
this.callbacks.onPause();
}
resume() {
if (this.state !== 'paused') return;
this.start();
}
stop() {
if (this.state === 'idle') return;
clearInterval(this.timerId);
this.state = 'idle';
}
reset() {
this.stop();
this.remaining = this.duration.clone();
this.callbacks.onTick(this.remaining);
}
getFormattedTime(format = 'mm:ss') {
// For a countdown timer, we often don't need the date part
const asMilliseconds = this.remaining.asMilliseconds();
// Handle negative time (shouldn't happen with our logic, but just in case)
const duration = asMilliseconds < 0
? moment.duration(0)
: this.remaining;
return moment.utc(duration.asMilliseconds()).format(format);
}
}
For React applications, here's a custom hook approach:
// useTimer.js
import { useState, useEffect, useRef, useCallback } from 'react';
export function useTimer(initialTime = 60, autostart = false) {
const [seconds, setSeconds] = useState(initialTime);
const [isActive, setIsActive] = useState(autostart);
const [isPaused, setIsPaused] = useState(false);
const countRef = useRef(null);
const startTimeRef = useRef(null);
const formatTime = useCallback(() => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
}, [seconds]);
const start = useCallback(() => {
setIsActive(true);
setIsPaused(false);
startTimeRef.current = Date.now() - ((initialTime - seconds) * 1000);
countRef.current = setInterval(() => {
const elapsedSeconds = Math.floor((Date.now() - startTimeRef.current) / 1000);
const remainingSeconds = initialTime - elapsedSeconds;
if (remainingSeconds <= 0) {
clearInterval(countRef.current);
setSeconds(0);
setIsActive(false);
} else {
setSeconds(remainingSeconds);
}
}, 100);
}, [initialTime, seconds]);
const pause = useCallback(() => {
clearInterval(countRef.current);
setIsPaused(true);
}, []);
const resume = useCallback(() => {
setIsPaused(false);
startTimeRef.current = Date.now() - ((initialTime - seconds) * 1000);
countRef.current = setInterval(() => {
const elapsedSeconds = Math.floor((Date.now() - startTimeRef.current) / 1000);
const remainingSeconds = initialTime - elapsedSeconds;
if (remainingSeconds <= 0) {
clearInterval(countRef.current);
setSeconds(0);
setIsActive(false);
} else {
setSeconds(remainingSeconds);
}
}, 100);
}, [initialTime, seconds]);
const reset = useCallback(() => {
clearInterval(countRef.current);
setSeconds(initialTime);
setIsActive(false);
setIsPaused(false);
}, [initialTime]);
// Clean up the interval on unmount
useEffect(() => {
return () => {
clearInterval(countRef.current);
};
}, []);
// Start the timer if autostart is true
useEffect(() => {
if (autostart) {
start();
}
}, [autostart, start]);
return {
seconds,
isActive,
isPaused,
start,
pause,
resume,
reset,
formatTime
};
}
Example usage in a React component:
import React from 'react';
import { useTimer } from './useTimer';
function TimerComponent() {
const {
seconds,
isActive,
isPaused,
start,
pause,
resume,
reset,
formatTime
} = useTimer(300); // 5-minute timer
return (
<div className="timer-container">
<h3>Timer: {formatTime()}</h3>
<div className="controls">
{!isActive && !isPaused ? (
<button onClick={start}>Start</button>
) : isPaused ? (
<button onClick={resume}>Resume</button>
) : (
<button onClick={pause}>Pause</button>
)}
<button onClick={reset} disabled={!isActive && !isPaused}>
Reset
</button>
</div>
{/* Progress bar */}
<div className="progress-container">
<div
className="progress-bar"
style={{ width: `${(seconds / 300) * 100}%` }}
/>
</div>
</div>
);
}
setInterval alone can cause drift over time. Our implementations counter this by using actual timestamps rather than counting on interval reliability.Worker threads or server-synced time.
Here's how to make a timer persist across page refreshes:
class PersistentTimer extends Timer {
constructor(durationInSeconds, onTick, onComplete, storageKey = 'persistent_timer') {
super(durationInSeconds, onTick, onComplete);
this.storageKey = storageKey;
this.load();
}
save() {
const state = {
duration: this.duration,
remaining: this.remaining,
isPaused: this.isPaused,
startTime: this.startTime,
timestamp: Date.now()
};
localStorage.setItem(this.storageKey, JSON.stringify(state));
}
load() {
const savedState = localStorage.getItem(this.storageKey);
if (savedState) {
const state = JSON.parse(savedState);
this.duration = state.duration;
// Calculate how much time has passed since the timer was saved
const elapsedSinceSave = Math.floor((Date.now() - state.timestamp) / 1000);
// Only reduce the remaining time if the timer wasn't paused
if (!state.isPaused) {
this.remaining = Math.max(0, state.remaining - elapsedSinceSave);
} else {
this.remaining = state.remaining;
this.isPaused = true;
}
// If the timer was running and hasn't completed, restart it
if (!this.isPaused && this.remaining > 0) {
this.start();
} else if (this.remaining <= 0) {
this.remaining = 0;
this.onComplete();
} else {
this.onTick(this.remaining);
}
}
}
start() {
super.start();
this.save();
// Add autosave every 5 seconds
this.autosaveId = setInterval(() => this.save(), 5000);
}
pause() {
super.pause();
this.save();
if (this.autosaveId) {
clearInterval(this.autosaveId);
this.autosaveId = null;
}
}
reset() {
super.reset();
this.save();
if (this.autosaveId) {
clearInterval(this.autosaveId);
this.autosaveId = null;
}
}
}
requestAnimationFrame instead of setInterval.
class RAFTimer {
constructor(onTick, onComplete) {
this.startTime = null;
this.duration = 0;
this.remaining = 0;
this.onTick = onTick || function() {};
this.onComplete = onComplete || function() {};
this.rafId = null;
this.isRunning = false;
this.lastTimestamp = 0;
}
start(durationInSeconds) {
if (this.isRunning) return;
this.duration = durationInSeconds;
this.remaining = durationInSeconds;
this.startTime = performance.now();
this.isRunning = true;
this.lastTimestamp = this.startTime;
const tick = (timestamp) => {
if (!this.isRunning) return;
// Calculate elapsed time in seconds
const elapsed = (timestamp - this.startTime) / 1000;
this.remaining = Math.max(0, this.duration - elapsed);
this.onTick(this.remaining);
if (this.remaining <= 0) {
this.isRunning = false;
this.onComplete();
} else {
this.rafId = requestAnimationFrame(tick);
}
this.lastTimestamp = timestamp;
};
this.rafId = requestAnimationFrame(tick);
}
stop() {
if (!this.isRunning) return;
cancelAnimationFrame(this.rafId);
this.isRunning = false;
}
pause() {
if (!this.isRunning) return;
cancelAnimationFrame(this.rafId);
this.isRunning = false;
// Record the current remaining time
const elapsed = (performance.now() - this.startTime) / 1000;
this.remaining = Math.max(0, this.duration - elapsed);
}
resume() {
if (this.isRunning) return;
// Adjust the start time to account for the pause
this.startTime = performance.now() - ((this.duration - this.remaining) * 1000);
this.isRunning = true;
const tick = (timestamp) => {
if (!this.isRunning) return;
const elapsed = (timestamp - this.startTime) / 1000;
this.remaining = Math.max(0, this.duration - elapsed);
this.onTick(this.remaining);
if (this.remaining <= 0) {
this.isRunning = false;
this.onComplete();
} else {
this.rafId = requestAnimationFrame(tick);
}
this.lastTimestamp = timestamp;
};
this.rafId = requestAnimationFrame(tick);
}
getFormattedTime() {
const minutes = Math.floor(this.remaining / 60);
const seconds = Math.floor(this.remaining % 60);
const ms = Math.floor((this.remaining % 1) * 100);
return `${minutes.toString().padStart(2, '0')}:${
seconds.toString().padStart(2, '0')}.${
ms.toString().padStart(2, '0')}`;
}
}
Adding timers and stopwatches to your web app isn't just about counting seconds—it's about creating a more engaging, useful experience for your users. The implementations provided range from simple to advanced, allowing you to choose the approach that best fits your application's needs.
Key takeaways:
Whether you're building a workout app, a cooking timer, a productivity tool, or an online assessment platform, these implementations provide a solid foundation for adding reliable timing functionality to your web application.
Explore the top 3 practical uses of timers and stopwatches in enhancing your web app’s functionality.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â