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

Usage

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

  1. Two independent state pairs. value / defaultValue track elapsed milliseconds; running / defaultRunning track whether the clock is ticking. Either pair can be controlled or left uncontrolled.
  2. setTimeout, not setInterval. Each tick schedules the next one and reads the actual wall-clock delta via performance.now(). This avoids the drift that browsers introduce when they coalesce interval timers in background tabs.
  3. Auto-pause on tab hide. A document.visibilitychange listener pauses the timer when the page is hidden during a run; it resumes on return unless the countdown completed in the meantime.
  4. Silent by default. No useSound peer dep. Chain onComplete to whatever sound, toast, or state machine you want.
  5. Tabular numerics. font-variant-numeric: tabular-nums keeps the mm:ss readout from jittering when digits change width.

Props

PropTypeDefaultDescription
durationMsnumberrequiredTotal countdown length, in milliseconds.
valuenumberControlled elapsed time (ms). Pair with onValueChange.
defaultValuenumber0Uncontrolled initial elapsed time.
onValueChange(msElapsed: number) => voidFires every tick (~250ms) with the new elapsed time.
runningbooleanControlled run state. Pair with onRunningChange.
defaultRunningbooleanfalseUncontrolled initial run state.
onRunningChange(running: boolean) => voidFires when the clock starts, pauses, auto-pauses, or completes.
format(msRemaining: number) => stringzero-padded mm:ssFormat the remaining time.
onComplete() => voidFires once when remaining time first reaches zero.
classNamestringMerged 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-label mirrors the current remaining time on every update — screen-reader queries always return a useful string.
  • aria-live is off by default. A counter that announces every second is unbearable; if you want announcements, pass aria-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-specific Timer data model and TimerRing companion; removed audio cues (useSound) so the v0 library version is silent; replaced the framer-motion card chrome with a flat headless surface so consumers compose their own card.