Docs
Timer

Timer

A flexible timer component system with compound components for displaying elapsed time. Features multiple variants, sizes, formats, and a custom hook for advanced timer functionality.

Installation

npx shadcn@latest add https://cult-ui.com/r/timer.json

Usage

import {
  Timer,
  TimerDisplay,
  TimerIcon,
  TimerRoot,
  useTimer,
} from "@/components/ui/timer"

Basic Timer

<Timer loading={true} />

Compound Components

For more flexibility, you can use the individual components:

<TimerRoot variant="outline" size="lg">
  <TimerIcon loading={true} />
  <TimerDisplay time="01:23" />
</TimerRoot>

Timer with Custom Format

<Timer loading={true} format="MM:SS" variant="outline" size="lg" />

Timer with Callback

import { useState } from "react"
 
const [isLoading, setIsLoading] = useState(false)
 
const handleTick = (seconds: number, milliseconds: number) => {
  console.log(`Elapsed: ${seconds}.${milliseconds}s`)
}
 
return (
  <div>
    <Timer
      loading={isLoading}
      onTick={handleTick}
      format="SS.MS"
      variant="outline"
    />
    <button onClick={() => setIsLoading(!isLoading)}>
      {isLoading ? "Stop" : "Start"}
    </button>
  </div>
)

Different Formats

// Seconds with milliseconds
<Timer loading={true} format="SS.MS" />
 
// Minutes:Seconds
<Timer loading={true} format="MM:SS" />
 
// Hours:Minutes:Seconds
<Timer loading={true} format="HH:MM:SS" />

API Reference

Timer Props

PropTypeDefaultDescription
loadingbooleanfalseWhether the timer is running/loading
onTick(seconds: number, milliseconds: number) => void-Callback called on each timer tick
resetOnLoadingChangebooleantrueWhether to reset timer when loading changes
format"SS.MS" | "MM:SS" | "HH:MM:SS""SS.MS"Time display format
variant"default" | "outline" | "ghost" | "destructive""default"Timer variant
size"sm" | "md" | "lg""md"Timer size
classNamestring-Additional CSS classes

TimerRoot Props

PropTypeDefaultDescription
variant"default" | "outline" | "ghost" | "destructive""default"Timer container variant
size"sm" | "md" | "lg""md"Timer size
loadingbooleanfalseWhether the timer is running
classNamestring-Additional CSS classes

TimerIcon Props

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Icon size
loadingbooleanfalseWhether to show loading state
iconReact.ComponentType<{ className?: string }>ClockCustom icon component
classNamestring-Additional CSS classes

TimerDisplay Props

PropTypeDefaultDescription
timestring-Time value to display
labelstring-Optional label for accessibility
size"sm" | "md" | "lg""md"Display size
classNamestring-Additional CSS classes

useTimer Hook

The Timer component uses the useTimer hook internally, which you can also use directly:

import { useTimer } from "@/components/ui/timer"
 
const {
  elapsedTime,
  milliseconds,
  formattedTime,
  isRunning,
  reset,
  start,
  stop,
} = useTimer({
  loading: true,
  onTick: (seconds, ms) => console.log(`${seconds}.${ms}s`),
  format: "SS.MS",
})

useTimer Options

OptionTypeDefaultDescription
loadingbooleanfalseWhether the timer is running
onTick(seconds: number, milliseconds: number) => void-Callback called on each timer tick
resetOnLoadingChangebooleantrueWhether to reset timer when loading changes
format"SS.MS" | "MM:SS" | "HH:MM:SS""SS.MS"Time display format

useTimer Return

PropertyTypeDescription
elapsedTimenumberElapsed time in seconds
millisecondsnumberCurrent milliseconds (0-999)
formattedTimeobjectFormatted time object with display
isRunningbooleanWhether the timer is currently running
reset() => voidReset the timer to 0
start() => voidStart the timer
stop() => voidStop the timer

Examples

Loading State Timer

const [isLoading, setIsLoading] = useState(false)
 
return (
  <div className="flex items-center gap-4">
    <Timer
      loading={isLoading}
      variant="outline"
      size="lg"
      format="SS.MS"
    />
    <button onClick={() => setIsLoading(!isLoading)}>
      {isLoading ? "Stop" : "Start"}
    </button>
  </div>
)

Timer with Progress Tracking

const [isLoading, setIsLoading] = useState(false)
const [elapsedTime, setElapsedTime] = useState(0)
 
const handleTick = (seconds: number, milliseconds: number) => {
  setElapsedTime(seconds)
  // Update progress bar, send analytics, etc.
}
 
return (
  <div className="space-y-4">
    <Timer
      loading={isLoading}
      onTick={handleTick}
      variant="ghost"
      size="lg"
      format="MM:SS"
    />
    <div className="text-sm text-muted-foreground">
      Elapsed: {elapsedTime} seconds
    </div>
    <button onClick={() => setIsLoading(!isLoading)}>
      {isLoading ? "Stop" : "Start"}
    </button>
  </div>
)

Different Time Formats

// For short durations (seconds with milliseconds)
<Timer loading={true} format="SS.MS" variant="default" />
 
// For medium durations (minutes:seconds)
<Timer loading={true} format="MM:SS" variant="outline" />
 
// For long durations (hours:minutes:seconds)
<Timer loading={true} format="HH:MM:SS" variant="ghost" />

Custom Timer with Compound Components

import { Pause, Play, RotateCcw } from "lucide-react"
 
import {
  TimerDisplay,
  TimerIcon,
  TimerRoot,
  useTimer,
} from "@/components/ui/timer"
 
function CustomTimer() {
  const { formattedTime, isRunning, start, stop, reset } = useTimer({
    format: "MM:SS",
  })
 
  return (
    <div className="flex items-center gap-4">
      <TimerRoot variant="outline" size="lg">
        <TimerIcon loading={isRunning} />
        <TimerDisplay time={formattedTime.display} />
      </TimerRoot>
 
      <div className="flex gap-2">
        <button onClick={isRunning ? stop : start}>
          {isRunning ? (
            <Pause className="w-4 h-4" />
          ) : (
            <Play className="w-4 h-4" />
          )}
        </button>
        <button onClick={reset}>
          <RotateCcw className="w-4 h-4" />
        </button>
      </div>
    </div>
  )
}

Custom Icon

import { Stopwatch } from "lucide-react"
 
import { Timer } from "@/components/ui/timer"
 
;<TimerRoot variant="destructive" size="lg">
  <TimerIcon loading={true} icon={Stopwatch} />
  <TimerDisplay time="02:15" />
</TimerRoot>

Using the Hook Directly

import { useTimer } from "@/components/ui/timer"
 
function CustomTimer() {
  const { elapsedTime, formattedTime, isRunning, start, stop, reset } =
    useTimer({
      loading: false,
      format: "MM:SS",
    })
 
  return (
    <div className="space-y-4">
      <div className="text-2xl font-mono">{formattedTime.display}</div>
      <div className="flex gap-2">
        <button onClick={isRunning ? stop : start}>
          {isRunning ? "Stop" : "Start"}
        </button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  )
}