Guide
Getting started
npm install @devix-labs/countdown
import { createCountdownWidget } from '@devix-labs/countdown';
import '@devix-labs/countdown/styles.css';
createCountdownWidget(document.querySelector('#sale'), {
deadline: '2026-12-31T23:59:59Z',
});
<dx-countdown deadline="2026-12-31T23:59:59Z" variant="clock"></dx-countdown>
What it sounds like
This is the part worth reading, because it is what everything else here gets wrong.
A countdown wired into an aria-live region announces the time every second.
On a page with a screen reader that is not a feature — it makes everything else
unusable, because the region interrupts continuously.
So the digits are aria-hidden inside a <time datetime="…">, and a separate
polite region speaks only when it is worth speaking: when the leading unit
changes — days to hours, hours to minutes — and at an hour, ten minutes, five,
one, thirty seconds, ten, and the last five.
Counting two minutes down to zero, it speaks fewer than fifteen times instead of a hundred and twenty.
createCountdownWidget(el, {
deadline,
announceAt: [60, 10, 3, 2, 1], // your own moments, in seconds
});
Three shapes
createCountdownWidget(el, { deadline, variant: 'boxes' }); // a number per unit
createCountdownWidget(el, { deadline, variant: 'clock' }); // 01:23:45
createCountdownWidget(el, { deadline, variant: 'words' }); // "2 days, 3 hours"
Leading units that are zero are dropped, so forty-five seconds is one box rather
than 00:00:00:45. Pass trim: false if you want them all.
It is right when the tab wakes up
Every value is worked out from deadline − now(), never counted up from ticks. A
counter that adds one per tick is wrong by however long the tab slept and has no
way to find out; this simply asks the clock.
const timer = createCountdownWidget(el, { deadline });
timer.read(); // { days, hours, minutes, seconds, total, done }
Following the server's clock, not the visitor's
A sale ending "in ten minutes" by your server has already ended on a machine whose clock is twenty minutes fast — and some are set that way deliberately.
import { serverClock, createCountdownWidget } from '@devix-labs/countdown';
const response = await fetch('/api/time');
const now = serverClock(Date.parse(response.headers.get('date')));
createCountdownWidget(el, { deadline, now });
serverClock works out the offset once and applies it from then on, so there is
no request per tick.
Any language
Everything goes through Intl:
createCountdownWidget(el, { deadline, locale: 'ar-EG', variant: 'clock' });
// ٠٢:٠٣:٠٤:٠٥
Arabic gets the dual — يومان for two days, which is a grammatical number English does not have — and Russian picks correctly among its three plural forms. No table of English words anywhere.