Countdown
Time remaining until a deadline, ticking. It counts toward a Date rather than decrementing a number, because setInterval is throttled in a background tab — a decremented timer drifts and then confidently shows the wrong number.
src/components/ui/countdown.tsxAbout
From the component's own source, where it was written.
Time remaining until a deadline, ticking.
For the things an agent or an API hands you an expiry for: a rate-limit reset, a signed URL, a token, a maintenance window, a hold on a booking.
**It counts toward a Date, not down from a number of seconds.** A duration decremented on an interval drifts — setInterval is throttled in a background tab and never fires on exactly its period — so a five-minute timer left in a background tab finishes minutes late and confidently displays the wrong number. Recomputing from the target each tick is correct whatever the interval actually did, and survives the tab being suspended entirely.
The interval is derived from what is displayed: a countdown showing seconds ticks every second, one showing only minutes ticks every fifteen. Waking the main thread once a second to redraw a number that changes once a minute is a battery cost with nothing to show for it.
At zero it stops and renders expiredLabel — it never shows negative time, which reads as a bug rather than as elapsed time.
Install
npx astralyx-ui add countdown
Usage
import { Countdown } from '@/components/ui/countdown'
<Countdown to={rateLimit.resetsAt} onExpire={refetch} />Composer
Change the props and the source below follows.
<Countdown
to={resetsAt}
showSeconds={true}
showDays={true}
onExpire={refetch}
/>Examples
Deadlines
<Countdown to={resetsAt} />
<Countdown to={windowClosesAt} showSeconds={false} />
<Countdown to={new Date(Date.now() - 1)} />API
Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.
Countdown
Extends Omit<ComponentProps<'span'>, 'children'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| to | Date | required | The deadline. Recomputed from the clock on every tick, so an interval that fired late — or not at all, in a suspended tab — cannot make it drift. |
| showSeconds | boolean | true | Also sets the tick rate: 1s when seconds are shown, 15s when they are not. Waking the main thread every second to redraw a number that changes every minute is a battery cost with nothing to show for it. |
| showDays | boolean | true | Off folds days into hours, so a two-day countdown does not read as 00:12:09. |
| expiredLabel | ReactNode | 'Expired' | Rendered once the deadline passes. |
| onExpire | () => void | — | Fires once, at zero. It never renders negative time; that reads as a bug rather than as elapsed time. |
| now | Date | — | A fixed reference instead of the clock — for tests and deterministic server rendering. |
| format | (parts: { days: number hours: number minutes: number seconds: number total: number }) => ReactNode | — | Receives { days, hours, minutes, seconds, total }. |