A countdown timer sounds like a beginner JavaScript project. Set an interval, decrement a number every second, display it. Three lines of code. Ship it.
Then you discover that setInterval drifts, the timer loses sync when the tab is backgrounded, and your "1 second" intervals are actually 1.002 seconds, accumulating to minutes of error over long countdowns.
Building a timer that actually works requires understanding how JavaScript handles time.
Why setInterval drifts
The fundamental problem is that setInterval does not guarantee timing precision. When you call setInterval(callback, 1000), you are saying "run this callback approximately every 1000 milliseconds, but not sooner." The browser's event loop, other tasks, garbage collection, and tab throttling all delay execution.
// The naive approach - DO NOT USE for anything that matters
let remaining = 3600; // 1 hour
setInterval(() => {
remaining--;
display(remaining);
}, 1000);
Over an hour, this timer might be off by 10-30 seconds. Over a day, it could be off by minutes. If the user switches to another tab, modern browsers throttle setInterval to once per second or even less, causing the timer to lose potentially minutes of real time.
The correct approach: wall-clock reference
Instead of counting intervals, compare against a fixed target timestamp. The current time minus the start time gives you elapsed time. The end time minus the current time gives you remaining time. This is always accurate regardless of interval drift.
const endTime = Date.now() + duration * 1000;
function update() {
const remaining = Math.max(0, endTime - Date.now());
const seconds = Math.floor(remaining / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
display(
hours,
minutes % 60,
seconds % 60
);
if (remaining > 0) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
Using requestAnimationFrame instead of setInterval gives smoother updates and automatically pauses when the tab is not visible, reducing CPU usage. When the user returns to the tab, the timer immediately shows the correct remaining time because it recalculates from the wall clock.
Handling page visibility
When a browser tab is backgrounded, requestAnimationFrame stops firing entirely. This is actually fine for our wall-clock approach -- the timer is still accurate when the user returns. But if you want to trigger actions at specific times (like an alarm), you need the Page Visibility API.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
update(); // Immediately refresh the display
checkAlarms(); // Check if any alarms should have fired
}
});
For timers that need to fire events while backgrounded (like notifications), you need a Service Worker or the less reliable setTimeout with self-correction.
Audio and notification at completion
When the timer reaches zero, playing an audio notification is straightforward but has one gotcha: mobile browsers require a user interaction before playing audio. You cannot play a sound on timer completion if the user has not interacted with the page.
The workaround is to initialize the audio context on the first user interaction (clicking the start button) and keep it ready.
let audioCtx;
startButton.addEventListener('click', () => {
audioCtx = audioCtx || new AudioContext();
// Start the timer...
});
For browser notifications, request permission when the user starts the timer, then fire a notification at completion even if the tab is backgrounded.
Sub-second precision
For countdown timers that display tenths of a second, requestAnimationFrame gives you approximately 16.67ms precision (60fps). For millisecond displays, this is sufficient -- the display refreshes fast enough that the user cannot perceive the rounding.
Do not try to achieve sub-millisecond precision in the browser. JavaScript's Date.now() and performance.now() have different precision characteristics, and browser fingerprinting mitigations intentionally reduce timer resolution.
The tool
For a countdown timer that handles all of these edge cases -- drift correction, tab backgrounding, audio notifications, and clean display -- I built one at zovo.one/free-tools/countdown-timer. Set your duration, start it, and it will accurately count down to zero even if you leave the tab.
I'm Michael Lip. I build free developer tools at zovo.one. 500+ tools, all private, all free.
Top comments (0)