Skip to content
Devix Open Source
UI component v1.0.0 MIT Alpha

Countdown

Drift-free countdown timers for launches and offers.

npm install @devix-labs/countdown
Vanilla JS React Vue Svelte WordPress
What a screen reader actually hears Open

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 an s on 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.

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.