Countdown
Drift-free countdown timers for launches and offers.
npm install @devix-labs/countdown
The one maintained option is React-only and contains the string 'aria' zero times; the vanilla alternatives were last published in 2015 and 2020. What everyone does instead is put the digits in an aria-live region, which announces the time every single second and makes the rest of the page unusable with a screen reader. Here the digits are aria-hidden inside a <time datetime>, and a separate polite region speaks only when the leading unit changes and at the milestones that matter — fewer than fifteen announcements over two minutes instead of a hundred and twenty, with a test that counts them. Every value is derived from the deadline rather than accumulated from ticks, so an hour of sleep costs nothing; serverClock() follows your server so a wrong visitor clock cannot end a sale early; and formatting runs through Intl, which gets the Arabic dual and Russian's three plurals right. 3.2 kB, zero dependencies, React, Vue, Svelte, a web component or none of them.
What you get
It does not announce every second
The digits are aria-hidden and a separate polite region speaks only when the leading unit changes and at an hour, ten minutes, five, one, thirty seconds, ten and the last five. Over two minutes that is under fifteen announcements rather than a hundred and twenty — and announceAt sets your own.
Right after the laptop was shut
Nothing is counted up from ticks. Every value is deadline minus now, so a tab that slept for an hour shows the truth the instant it wakes, and onComplete still fires exactly once even when the clock jumped clean over the deadline.
Your server's clock, not the visitor's
serverClock() works out the offset once from a timestamp you render into the page and applies it from then on, so a machine twenty minutes fast does not see the sale end early — or keep buying twenty minutes after it closed.
Every language, through Intl
Arabic-Indic digits where the locale asks for them, the Arabic dual for two days, and Russian's three plural forms — rather than an English word list with an s on the end. RTL comes from logical properties, so the layout mirrors itself.
It wakes when the number changes
There is no thousand-millisecond interval. Each tick schedules the next to land just after the display actually changes, so a countdown showing days is nearly free and one showing hundredths wakes when it must. A hidden tab schedules nothing at all.
Framework-free, then every framework
One DOM core with React, Vue, Svelte, a dx-countdown element and a CDN build over it — so a Laravel page, an Astro page and a React app get identical behaviour instead of three different libraries.
Three shapes, and the parts underneath
A box per unit, a clock, or a sentence — with leading zero units dropped, optional labels, daysInHours, sub-second precision and overtime counting upwards past the deadline. partsOf, clock, words and relative are exported on their own and are pure, so they render on a server too.
3.2 kB, nothing else
Zero dependencies, tree-shakable ESM, a stylesheet built entirely out of custom properties, classNames on every part, theme auto, and a @layer devix build so your own CSS wins without a fight.
Countdown — overview
Why it exists
This is a smaller market than most in this catalogue and it is worth saying so: three hundred thousand downloads a week between all of them, not twenty million.
But the shape is familiar. The one maintained option, react-countdown at 248 k
a week, is React-only. The vanilla alternatives were last published in 2015
and 2020, the newer of which has 33 open issues and does not ship ES modules.
A Laravel, Astro or plain page has no good answer.
And there is one thing none of them does at all.
The accessibility problem nobody has solved
react-countdown contains the string aria zero times. It renders numbers
and leaves the rest to you.
What people do instead is put the digits in an aria-live region — and that
announces the time every single second. A screen reader then interrupts
continuously, and the rest of the page becomes unusable. It is worse than no
announcement at all.
So here the digits live in an aria-hidden <time datetime="…">, and a separate
polite region speaks only when it is worth speaking: when the leading unit
changes, and at an hour, ten minutes, five, one, thirty seconds, ten and the last
five. Over a two-minute countdown that is fewer than fifteen announcements
instead of a hundred and twenty, and there is a test that counts them.
What else it does differently
- Every language, through
Intl. Arabic-Indic digits where the locale asks, the Arabic dual for two days, and Russian's three plural forms — rather than a table of English words with anson the end. - Framework-free, with React, Vue, Svelte, a web component and a CDN build over one core.
- Ticks scheduled to the boundary, so a countdown showing days does not wake every second and one showing hundredths wakes when it must. Nothing is scheduled at all while the tab is hidden.
serverClock(), so a visitor whose machine is twenty minutes fast does not see the sale end early.- 3.2 kB, no dependencies.
What we do not claim
That setInterval drifts badly. It is repeated everywhere, and we could not
reproduce it — a naive interval on a busy page, and one left in a background tab,
both stayed accurate under test. Real browsers throttle hidden tabs harder than a
headless one does, so the effect is real, but it is not ours to assert without
having measured it.
What we do claim, and test, is the property that makes it moot: every value is
derived from deadline − now(), never accumulated. A test moves the clock
forward an hour without firing a single tick and asserts the widget is
immediately right. react-countdown gets this right too — it is the correct
baseline, not a differentiator.
Not in 1.0
A flip-clock animation. It is what flipdown is for, it is mostly CSS, and
baking one in would triple the stylesheet for an effect most pages do not want.
The parts are exposed, so building one over this core is a stylesheet rather than
a fork.
Demos
All demos →How it compares
Questions
Why is announcing the time every second a problem?
Because a polite live region queues. A screen reader that is handed a new string every second spends the entire countdown reading numbers and never gets to the rest of the page, which is worse than announcing nothing at all. Milestones give someone the information they actually need — an hour, five minutes, the last ten seconds — and leave the page usable in between.
Does setInterval really drift?
We could not reproduce it, and we do not claim it. A naive interval stayed accurate under test both on a busy page and in a background tab, though a real browser throttles hidden tabs harder than a headless one does. What we do claim, and test, is the property that makes the question moot: every value is derived from the deadline, so a tab that slept for an hour is right immediately rather than an hour behind.
What should I send from the server?
The deadline, as ISO 8601 with an offset — toIso8601String(), never a formatted local string. A countdown is the one place a missing timezone becomes hours of error rather than a cosmetic problem. If the page is cached, render the deadline and let the browser do the arithmetic, since a remaining time is stale the moment it is sent.
Is there a flip-clock animation?
No. That is what flipdown is for, it is almost entirely CSS, and baking one in would triple the stylesheet for an effect most pages do not want. The parts are exposed and every piece of the markup takes a class of yours, so a flip animation over this core is a stylesheet rather than a fork.
Can I use just the maths?
Yes — partsOf, clock, words, relative and largestUnit are pure functions with no DOM, exported from @devix/countdown/core at 1.4 kB. createCountdown() is the engine on its own, with onTick and onComplete and no markup at all.