Time remaining, rendered as a time element.
import { Countdown } from '@oztix/roadie-components/countdown'
Counts minutes while there is time, then switches to a clock under five minutes. See Date and time.
<Countdown until={new Date(Date.now() + 8 * 60000)} />
A ticking clock is not neutral. It changes how the wait feels, and the direction depends on what is at the end of it.
The labels sit in their own column. With three rows of flex the ticking count pushes its own label sideways, so the one thing the example is comparing moves while you read it.
Past an hour, a ticking count breaks into segments so the scale stays visible. A bare clock would bury the days.
<Countdown until={new Date(Date.now() + 3 * 86400000 + 4 * 3600000 + 12 * 60000)} seconds='always' />
display picks the shape of the count. Leave it on auto, which reads the
time remaining and switches for you: segments above an hour, a clock below it,
and a coarse count when the seconds are not ticking.
Set it only where the shape must not move. A dashboard column stays legible if every row is the same shape, even when one of them is hours from expiring.
<div className='grid gap-2'> <Countdown until={new Date(Date.now() + 3 * 86400000)} display='coarse' /> <Countdown until={new Date(Date.now() + 3 * 86400000)} display='clock' /> <Countdown until={new Date(Date.now() + 3 * 86400000)} display='segments' /> </div>
Under five minutes the coarse count gives way to a clock. This one starts inside that window.
<Countdown until={new Date(Date.now() + 4 * 60000 + 32000)} />
The coarse register uses the same words as Duration and formatDuration.
This only reaches the coarse register. The segmented display always uses single letters, because they sit against the digits rather than in a sentence.
<Countdown until={new Date(Date.now() - 1000)} expiredLabel='Expired' />
A value that cannot be read as an instant reads as expired too. Failing to a deadline that has passed is safer than failing to one that has not.
Colour belongs on the container, never on the digits. A Badge or a Card carries intent. A time element is text, and colouring it directly means every surface invents its own scale.
useCountdownUrgency shares the same page-wide ticker, so escalating costs no extra timer. It only changes at a threshold, so the parent re-renders three times across a whole countdown rather than once a second.
Drive the intent from the same moment, so the badge and the digits never disagree:
The thresholds are five minutes to warning and two minutes to danger, matching the cart. Override them per surface where the window is a different shape.
A countdown is interface, not prose, so it takes the ui display scale. The prose scale is for article headings and runs larger than anything an interface needs.
Size says how much attention the moment deserves, and most countdowns are small. A row in a list wants text-sm inside a Badge. The most prominent one on a page tops out around text-display-ui-4. Past that a countdown stops reading as interface.
The countdown inherits type from its container, so there is nothing to configure. Set the scale on the parent and the digits follow.
A registration opening is the reason somebody is on the page, so it reads as a heading. Run it inline at text-display-ui-4, with the count picking up the accent colour.
Note it sits inside the heading rather than under a label. The count is part of the sentence, so it inherits the heading's size and only the colour changes.
Pick seconds from what is at the end of the wait, not from how long it is.
| What is at the end | seconds |
|---|---|
| Something you might lose, like a held cart | urgent |
| Something you are waiting for, like a registration opening | always |
| Something measured in hours or days | never |
Counting seconds on a held cart creates anxiety. The reader might lose something, and watching the seconds go makes an ordinary wait feel like a threat. Counting seconds on a registration opening creates anticipation. The reader is waiting for something good, and the seconds are the point.
| Decision | Where it goes |
|---|---|
| Colour and urgency | The container, via intent |
| Size | The container, via the display scale |
| Emphasis and surface | The container, via emphasis |
| Digit alignment | Handled. Do not override tabular-nums |
Three things follow from that.
Do not set a colour on the countdown itself. An escalating cart badge changes the Badge's intent, so the text, the border and the indicator all move together. Colouring the digits alone leaves the container behind and the pairing breaks.
Do not set a font size on the digits. The component inherits, so a parent on text-display-ui-4 gives a prominent countdown and a parent on text-sm gives an inline one. Sizing the digits alone breaks the baseline against neighbouring text.
Reach for the ui scale, never prose. A countdown is interface even when it sits inside a heading, and the prose scale is tuned for running text at sizes no interface needs.
Do not override tabular-nums or set a width. Each field already sits in a fixed character box, which is what stops a label beside the clock moving as digits roll.
A queue or a throttle is a third case. The wait is short, the seconds tick, and something happens automatically at zero. It is neither a threat nor a treat, so the tone should be plain.
A number counting down on its own is a puzzle. The reader needs to know whether to sit still or do something.
Checking again in 0:20Your place is held.
Do
The count and the consequence together. Nobody has to guess whether to reload.
0:20
Don’t
A bare number. Is it a deadline? Does the reader need to act?
Never make the countdown responsible for the thing that happens. It is a display, it stops while the tab is hidden, and whatever occurs at zero must occur on its own, with the count reporting rather than driving.
Countdown needs a timer and a render loop, so there is no server-rendered
equivalent. A template can only print the moment the count runs out, which is
DateTime rather than a countdown.
Countdown itself words its coarse register with formatDuration, the same
function Duration uses. Two more are exported for the cases that are not a
ticking clock on a page.
Reach for formatCountdown where the output is a string rather than an
element, such as a document title counting down in a tab. Reach for
countdownUrgency to pick a container's intent outside a render, where
useCountdownUrgency cannot run. It takes the same thresholds as the hook and
returns expired for anything at or below zero.
The visible clock ticks. What is spoken does not.
Progress is announced through a visually hidden live region, on a minute boundary at most. A live region that fires every second interrupts the screen reader continuously and drowns out the rest of the page. The digits themselves are aria-hidden, so nothing is announced twice.
Digits carry tabular-nums, so the row does not jitter as values change. Animation respects the reader's motion preference, which NumberFlow handles by default.
A countdown runs for as long as the page is open, so the cost compounds. Three things keep it cheap, and this component does all three.
Every countdown on the page shares one interval, so a list of twenty cards starts one timer rather than twenty. The timer stops entirely while the tab is hidden. And the value React watches is bucketed to what is on screen, so a coarse countdown re-renders once a minute rather than sixty times.
That last one is the difference that matters. Subscribing to raw seconds re-renders every second no matter what is displayed.
Urgency for a moment, updated as it approaches. Shares the one page-wide ticker, so escalating a container's intent costs no extra timer.
| Param | Type | Description |
|---|---|---|
until | Instantish | The moment being counted to. |
thresholds | UrgencyThresholds | Optional warnBelowMs / dangerBelowMs overrides. Defaults to five and two minutes. |
Returns 'success' \| 'warning' \| 'danger' \| 'expired'.
const urgency = useCountdownUrgency(until)
Time remaining, rendered as a `time` element. Performance is the reason this is a component rather than a pattern. Every countdown on the page shares one interval, it stops while the tab is hidden, and the snapshot is bucketed to the value actually displayed. In the coarse register that means one render a minute rather than sixty.
The moment being counted to. A value that cannot be read as an instant reads as expired, never as plenty of time.
When to show a ticking clock rather than a coarse count of minutes. - `urgent` (default) counts minutes until the deadline is close, then switches. For a held cart or a closing sale, where a clock running from the start manufactures anxiety the situation does not warrant. - `always` ticks from the start. For something a person is waiting *for*, like a registration or an on sale, where the seconds are the point. - `never` stays coarse. For a countdown measured in hours or days.
Defaults to urgent.
The shape of the count. - `auto` (default) picks one: words when it is not ticking, a clock when it is and there is under an hour left, segments beyond that. - `segments` is the days / hours / mins / secs breakdown. For a long wait somebody is looking forward to, where losing the days would be absurd. - `clock` is `4:32`, growing an hour field when it needs one. - `coarse` is words only.
Defaults to auto.
How the coarse register words itself. `long` reads '8 minutes', `medium` reads '8 mins', `short` reads '8m'. Matches Duration and formatDuration. The segmented display is unaffected: its units are always single letters, because they sit against the digits rather than in a sentence.
Threshold for `seconds='urgent'`. Defaults to five minutes.
Defaults to 5 * 60_000.
Rendered once the moment has passed. Without it the countdown falls through to its usual shape at zero, so a clock reads '0:00' and a coarse register reads '0 minutes'. Prefer a word. 'Expired' or 'Closed' tells the reader what happened, where a stopped clock leaves them to work it out.
Announce progress to assistive tech, coarsely. Defaults to true. Never per second: that is unusable.
Defaults to true.