An SSR-safe timer with controlled or local seconds, pause support, and a completion announcement.
Your session closes in
Save your work before the timer ends.
import { Countdown } from "@/components/ballmac/countdown"
export default function CountdownDemo() {
return (
<div className="bg-card flex w-full max-w-xs flex-col items-center gap-3 rounded-xl border border-border p-5">
<p className="text-sm font-medium">Your session closes in</p>
<Countdown defaultValue={600} label="Session remaining" />
<p className="text-muted-foreground text-xs">
Save your work before the timer ends.
</p>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/countdownAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/countdown.tsx// Ballmac UI: Countdown. https://ui.ballmac.com/components/countdown "use client" import * as React from "react" import { cn } from "@/lib/utils" import { useLocale, useMessages } from "@/lib/ballmac/i18n" type CountdownProps = Omit<React.ComponentProps<"div">, "defaultValue"> & { /** Controlled seconds remaining. */ value?: number /** Initial seconds remaining when uncontrolled. */ defaultValue?: number /** Called after a one-second tick. */ onValueChange?: (seconds: number) => void /** Called once when an uncontrolled timer reaches zero. */ onComplete?: () => void /** Pause or resume ticking. */ running?: boolean /** Accessible purpose of the timer. */ label?: string } function Countdown({ className, value, defaultValue = 0, onValueChange, onComplete, running = true, label, ...props }: CountdownProps) { const msg = useMessages() const locale = useLocale() label ??= msg("countdown.label", "Time remaining") const [internal, setInternal] = React.useState( Math.max(0, Math.floor(defaultValue)), ) const remaining = Math.max(0, Math.floor(value ?? internal)) const onValueChangeRef = React.useRef(onValueChange) const onCompleteRef = React.useRef(onComplete) React.useEffect(() => { onValueChangeRef.current = onValueChange onCompleteRef.current = onComplete }, [onValueChange, onComplete]) React.useEffect(() => { if (!running || remaining === 0) return const timer = window.setTimeout(() => { const next = Math.max(0, remaining - 1) if (value === undefined) setInternal(next) onValueChangeRef.current?.(next) if (next === 0) onCompleteRef.current?.() }, 1000) return () => window.clearTimeout(timer) }, [running, remaining, value]) const hours = Math.floor(remaining / 3600) const minutes = Math.floor((remaining % 3600) / 60) const seconds = remaining % 60 const units = hours > 0 ? ([ [hours, "hours"], [minutes, "minutes"], [seconds, "seconds"], ] as const) : ([ [minutes, "minutes"], [seconds, "seconds"], ] as const) const unitFormat = (n: number, unit: "hour" | "minute" | "second") => new Intl.NumberFormat(locale, { style: "unit", unit, unitDisplay: "long" }).format(n) const spoken = new Intl.ListFormat(locale, { type: "unit", style: "short" }).format([...(hours ? [unitFormat(hours, "hour")] : []), unitFormat(minutes, "minute"), unitFormat(seconds, "second")]) return ( <div data-slot="countdown" role="timer" aria-label={`${label}: ${spoken}`} className={cn( "inline-flex min-w-0 items-center gap-1.5 font-mono tabular-nums", className, )} {...props} > {units.map(([number, unit], index) => ( <React.Fragment key={unit}> {index > 0 && ( <span aria-hidden="true" className="text-muted-foreground text-lg"> : </span> )} <span aria-hidden="true" data-slot="countdown-unit" className="bg-card flex min-w-11 items-center justify-center rounded-lg border border-border px-2 py-1.5 text-lg font-semibold shadow-sm" > {String(number).padStart(2, "0")} </span> </React.Fragment> ))} <span className="sr-only" aria-live="polite"> {remaining === 0 ? msg("countdown.complete", "{label} complete", { label }) : ""} </span> </div> ) } export { Countdown, type CountdownProps }Update the import paths to match your project setup.
Usage
import { Countdown } from "@/components/ballmac/countdown"The full example is in the Code tab above.
Examples
States and variants
import { Countdown } from "@/components/ballmac/countdown"
export default function CountdownStates() {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-4">
<Countdown value={3670} running={false} label="Event starts in" />
<Countdown value={0} running={false} label="Time remaining" />
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
valueControlled seconds remaining. | number | — |
defaultValueInitial seconds remaining when uncontrolled. | number | 0 |
onValueChangeCalled after a one-second tick. | (seconds: number) => void | — |
onCompleteCalled once when an uncontrolled timer reaches zero. | () => void | — |
runningPause or resume ticking. | boolean | true |
labelAccessible purpose of the timer. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| None | Timer is labelled; completion is announced |
Use with AI
An SSR-safe timer with controlled or local seconds, pause support, and a completion announcement. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Countdown (@ballmac/countdown) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Show time until a short event
- Display a temporary offer or session limit
Not for
- Use progress for work completion
Registry JSON: https://ui.ballmac.com/r/countdown.json
Credits
Free to use in personal and commercial projects.
- npm
- None
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Progress Ring
A token-colored circular progress meter with a readable center and accessible value.
Alert Dialog
Focus-managed confirmation for consequential actions, with a clear cancel path, optional media, and a token-based destructive action.
Alert
A semantic callout with five theme-aware tones, clear icon placement, an action row, and opt-in urgent announcements.
Banner
A page-wide announcement with semantic tones, optional action, and controlled or local dismissal.