Count Down to a Future Date in JavaScript

To count down to a future moment, subtract now from the target as a Temporal.Duration and balance it into days/hours/minutes/seconds with round. Part of Temporal.Duration Arithmetic.

Why this scenario is tricky

A countdown looks like simple subtraction — target minus now — but two things make it subtle. The first is that the difference must be balanced into human-meaningful units: not "4,271,999 seconds" but "49 days, 10 hours, 39 minutes, 59 seconds." Computing that breakdown by hand with modulo arithmetic is where bugs creep in, especially around the boundaries where a component rolls over. The second is that a countdown is live — it is recomputed every tick against a moving "now" — so it must handle the moment the target passes, transitioning cleanly to zero (or a "past due" state) rather than flipping to negative components or a confusing display.

Temporal handles the balancing for you: since (or until) with a largestUnit option returns a Temporal.Duration already broken into the units you asked for, correctly balanced, so you never write the modulo chain. The remaining design decisions are which units to show, which reference frame to measure in, and how to detect and present the target having passed. Getting those right is what separates a countdown that stays correct for weeks from one that glitches at midnight or after a daylight-saving change.

Naive countdowns compute targetMs - Date.now() and then do modular arithmetic by hand, which drifts and mishandles the final tick. Temporal gives you the signed gap as a Duration and balances it into whole units, so the display reaches exactly 0d 0h 0m 0s at the target.

Balance the gap, don't hand-roll moduloManual ms modulo drifts and skips the last tickBalance the gap, don't hand-roll modulotargetMs - now, then % mathoff-by-one at zerotarget.since(now).round(...)balanced unitsLet Temporal.since produce and balance the remaining duration.

Minimal working solution

target.since(Temporal.Now.instant(), { largestUnit: 'day' }) gives the remaining time from now to the target as a Duration whose largest unit is days, so you can read .days, .hours, .minutes, and .seconds directly for display. Measuring against Temporal.Now.instant() — an absolute instant — is the right frame for a countdown to a fixed global moment like a product launch or a New Year that happens at one instant worldwide. The largestUnit cap of day keeps the display in a familiar range rather than expressing everything in hours or, worse, raw seconds.

Each tick you recompute the difference against a fresh now, and the Duration updates accordingly. Because since does the balancing, the components always add up consistently — decrementing the seconds rolls the minutes at exactly the right moment — with no off-by-one at the boundaries. The original target never changes; only the moving reference advances, which is the clean mental model of a countdown as a pure function of the current instant.

import { Temporal } from '@js-temporal/polyfill';
const target = Temporal.Instant.from('2026-12-31T23:59:59Z');
const d = target.since(Temporal.Now.instant(), { largestUnit: 'day' });
// d.days, d.hours, d.minutes, d.seconds

Remaining durationsince(now) with a largest unitRemaining durationtarget, nowsincelargestUnitd/h/m/s

Full production version

The production countdown adds two things the minimal version lacks: a past-due guard and a choice of reference frame. Comparing the computed Duration against a zero duration with Temporal.Duration.compare tells you whether the target has passed, so you can clamp the display to zero and switch to a "started" or "expired" state instead of rendering negative components. Doing this explicitly avoids the common glitch where a countdown briefly shows "-1 seconds" or garbled values in the tick right after the deadline.

The reference-frame choice matters when the countdown is to a civil moment rather than a global instant. "Midnight on New Year's Eve" happens at different instants in different zones, so a countdown to the user's local New Year should measure against a ZonedDateTime target in the user's zone, not a single UTC instant. Similarly, a "days until your birthday" countdown is a calendar-distance question best measured on PlainDate with largestUnit: 'day', so daylight-saving transitions never make the day count wobble. Picking the frame that matches what the user is counting down to is the design decision that keeps the number meaningful.

import { Temporal } from '@js-temporal/polyfill';
function remaining(target: Temporal.Instant) {
  const diff = target.since(Temporal.Now.instant(), { largestUnit: 'day', smallestUnit: 'second' });
  return Temporal.Duration.compare(diff, {}) <= 0
    ? { days: 0, hours: 0, minutes: 0, seconds: 0 } // clamp once elapsed
    : diff;
}

Clamp at zeroReturn zeros once the target passesClamp at zerotargetsince nowelapsed?clamp / diff

Verification snippet

Because the countdown depends on "now," test it by injecting a fixed reference instant rather than reading the real clock, so the assertions are deterministic. With a target and a frozen now a known distance apart, assert the Duration reports the exact expected days, hours, minutes, and seconds, and that the components balance (no field exceeds its natural range). Move the frozen now to one second before the target and assert a one-second remainder; move it to the target exactly and assert zero; move it past and assert the past-due guard fires.

Add a balancing boundary case — a distance like "1 day and 0 hours minus 1 second" — and assert the breakdown rolls over correctly to "0 days, 23 hours, 59 minutes, 59 seconds," which is exactly where hand-rolled modulo math tends to slip. If the countdown targets a civil moment, assert that measuring in the intended zone across a daylight-saving transition still yields the correct whole-day count. These frozen-clock tests prove the logic without any dependence on when the suite runs.

Countdown Timer assertionsKey cases assert correctlyAssertions that prove the edge casetarget in futurepositive unitsbalancedseconds < 60at targetall zeropast targetclamped 0

Common pitfalls

The first pitfall is computing the breakdown by hand with modulo arithmetic instead of letting since/until balance it, which invites off-by-one errors at unit boundaries. The second is failing to guard the past-due case, so the countdown renders negative or garbled components the instant the target passes; compare against a zero duration and clamp. The third is measuring a civil countdown against a global UTC instant — "local midnight" is not one instant worldwide — which makes the countdown finish at the wrong local time for users outside the reference zone.

A related mistake is reading the real clock directly inside the countdown logic, which makes it impossible to test deterministically; take the reference instant as an input so a test can freeze it. Finally, be deliberate about largestUnit: capping at days is right for a launch countdown, but a "months until" display wants largestUnit: 'month' measured on a calendar type with a relativeTo anchor, because months are variable-length and cannot be balanced from a bare instant. Choosing the wrong largest unit produces either an unwieldy hours-only display or an incorrectly balanced one.

Countdown Timer pitfallsCommon mistakes and their fixesWrongRighttargetMs - now with % mathdrifts, last tick offsince().round balancedexact unitsforget to clampnegative countdownclamp once elapsedreads 0 at target

Frequently Asked Questions

How do I build a countdown timer in JavaScript?

Compute target.since(Temporal.Now.instant(), { largestUnit: 'day' }) on each tick to get the remaining time as a balanced Temporal.Duration, then display its days, hours, minutes, and seconds. Re-evaluate on an interval and clamp to zero once the target passes.

Why does my countdown skip or repeat the last second?

Hand-rolled modulo on a millisecond gap accumulates rounding error and mishandles the boundary at zero. Using Temporal.since with a smallestUnit of 'second' balances the remaining time into whole units so it lands exactly on zero.

How do I build a countdown to a future date in JavaScript?

Compute target.since(Temporal.Now.instant(), { largestUnit: 'day' }) to get a balanced Duration and read .days, .hours, .minutes, and .seconds for display, recomputing each tick against a fresh now. Temporal balances the units for you, so the components always add up and roll over correctly. Guard the moment the target passes by comparing the Duration against zero and clamping to a done state.

How do I stop a countdown from showing negative numbers after the deadline?

Compare the computed Duration against a zero duration with Temporal.Duration.compare each tick; when it is zero or negative, clamp the display to zero and switch to an expired or started state instead of rendering the raw components. Without this guard the countdown briefly shows negative or garbled values in the tick right after the target passes.

Should a countdown to local midnight use an instant or a zoned target?

Use a zoned target in the user's time zone, because local midnight happens at a different instant in each zone. Measuring against a single UTC instant would make the countdown finish at the wrong local time for anyone outside the reference zone. For a global moment that occurs everywhere at once, like a synchronized launch, a single Instant target is correct.