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.tsx

About

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.

01:14:09
Props
tsx
<Countdown
  to={resetsAt}
  showSeconds={true}
  showDays={true}
  onExpire={refetch}
/>

Examples

Deadlines

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'>.

PropTypeDefaultDescription
toDaterequiredThe 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.
showSecondsbooleantrueAlso 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.
showDaysbooleantrueOff folds days into hours, so a two-day countdown does not read as 00:12:09.
expiredLabelReactNode'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.
nowDate—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 }.