Timer
A countdown timer that ticks every ~250ms, formats the remaining time, and auto-pauses when the document is hidden. Headless and silent by default — wire your own buttons for pause/resume/reset, and chain a chime onto onComplete if you want sound.
00:15
Customize
Duration
15s
Display
State
Installation
npx shadcn@latest add https://craftbits.dev/r/timer.jsonUsage
import { Timer } from "@craft-bits/core";
<Timer durationMs={60_000} defaultRunning />Controlled:
const [running, setRunning] = useState(false);
const [elapsed, setElapsed] = useState(0);
<Timer
durationMs={60_000}
running={running}
onRunningChange={setRunning}
value={elapsed}
onValueChange={setElapsed}
onComplete={() => playChime()}
/>Custom format (seconds only):
<Timer
durationMs={10_000}
format={(ms) => `${Math.ceil(ms / 1000)}s`}
defaultRunning
/>Understanding the component
- Two independent state pairs.
value/defaultValuetrack elapsed milliseconds;running/defaultRunningtrack whether the clock is ticking. Either pair can be controlled or left uncontrolled. setTimeout, notsetInterval. Each tick schedules the next one and reads the actual wall-clock delta viaperformance.now(). This avoids the drift that browsers introduce when they coalesce interval timers in background tabs.- Auto-pause on tab hide. A
document.visibilitychangelistener pauses the timer when the page is hidden during a run; it resumes on return unless the countdown completed in the meantime. - Silent by default. No
useSoundpeer dep. ChainonCompleteto whatever sound, toast, or state machine you want. - Tabular numerics.
font-variant-numeric: tabular-numskeeps themm:ssreadout from jittering when digits change width.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
durationMs | number | required | Total countdown length, in milliseconds. |
value | number | — | Controlled elapsed time (ms). Pair with onValueChange. |
defaultValue | number | 0 | Uncontrolled initial elapsed time. |
onValueChange | (msElapsed: number) => void | — | Fires every tick (~250ms) with the new elapsed time. |
running | boolean | — | Controlled run state. Pair with onRunningChange. |
defaultRunning | boolean | false | Uncontrolled initial run state. |
onRunningChange | (running: boolean) => void | — | Fires when the clock starts, pauses, auto-pauses, or completes. |
format | (msRemaining: number) => string | zero-padded mm:ss | Format the remaining time. |
onComplete | () => void | — | Fires once when remaining time first reaches zero. |
className | string | — | Merged onto the root <div> via cn(). |
Accessibility
- The root element has
role="timer"so assistive tech identifies it as a live elapsed-time control. aria-labelmirrors the current remaining time on every update — screen-reader queries always return a useful string.aria-liveis off by default. A counter that announces every second is unbearable; if you want announcements, passaria-live="polite"and announce only on milestones / completion.- The component itself is non-interactive — wrap it with your own
<button>s for pause/resume/reset and label them accordingly.
Credits
- Extracted from:
terminal-dreams(src/components/cookbook/Timer.tsx). Generalized: stripped the project-specificTimerdata model andTimerRingcompanion; removed audio cues (useSound) so the v0 library version is silent; replaced theframer-motioncard chrome with a flat headless surface so consumers compose their own card.