Countdown
A live countdown to a deadline. Formats itself for the current locale, announces itself as a timer rather than shouting every tick at screen readers, and hands you the raw units when you want to lay them out yourself.
Pass a deadline and it ticks once a second until it reaches zero. Seed the deadline in state rather than computing it inline — Date.now() + n in JSX is recomputed on every render, which resets the countdown instead of advancing it.
Import
import { Countdown } from "@mikenotthepope/substrateui"Formatting
The default output comes from Intl.DurationFormat, so it follows the locale and drops units that are still zero — 04:05, then 3:04:05, then 2 days, 3:04:05. Pass locale to pin it to a specific one.
No format string to learn — the locale decides.
Custom layout
Pass a function as children to lay the units out yourself. This replaces a format-string DSL: you get the numbers and render whatever you like, including a different message once finished flips.
In a StatCard
StatCard takes any node as its value, so a countdown drops straight in — the metric card and the live timer, with no third component in between.
Sale ends
The hook
useCountdown is the same logic without the markup, for when the remainder drives something other than text.
Import from @mikenotthepope/substrateui/hooks.
Labels
The timer's digits are announced without context unless this names what is counting down. Override one instance with the labels prop on the component, or every instance at once through LabelsProvider's countdown key — the provider is how you translate the set once instead of at each call site.
| Prop | Type | Default | Description |
|---|---|---|---|
remaining | string | "Time remaining" | Accessible name for the timer region. |
Accessibility
The element is a role="timer", whose implicit aria-live is off. That is deliberate: a polite live region here would read the clock aloud once a second. Screen reader users get the value on demand instead, which is why the timer carries an accessible name — "Time remaining" by default, overridable through labels.remaining.
Server-rendered output is empty until the countdown is measured in the browser. The server cannot know what "now" is on the client, and rendering a clock during hydration guarantees a mismatch.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
deadline | Date | number | — | When the countdown reaches zero — a Date or epoch milliseconds. |
interval | number | 1000 | Tick rate in milliseconds. |
locale | string | — | BCP-47 locale for the formatted remainder. Defaults to the runtime locale. |
onFinish | () => void | — | Fired once when the deadline is reached, and again only if a new deadline is set. |
children | (state: CountdownState) => React.ReactNode | — | Render prop receiving the live state, replacing the default formatted string. |
labels | CountdownLabels | — | Override the accessible name via labels.remaining. |
CountdownState
The object handed to the render prop, and returned by useCountdown.
| Prop | Type | Default | Description |
|---|---|---|---|
total | number | — | Milliseconds left, clamped at 0. |
days / hours / minutes / seconds | number | — | The remainder split into whole units. |
formatted | string | — | The remainder formatted for the locale, e.g. "2 days, 3:04:05". |
finished | boolean | — | True once the deadline has passed. |
ready | boolean | — | False until the countdown has been measured in the browser. |