Fiber UI LogoFiberUI

useCountdown

Manage timer state for countdowns and intervals.

A hook for creating countdown timers, interval counters, and stopwatches. It handles start, stop, reset, and completion callbacks.

Source Code

View the full hook implementation in the Hook Source Code section below.

Basic Timer

A simple 10-second countdown with start, stop, and reset controls.

10s
"use client";

import { useCountdown } from "@repo/hooks/utility/use-countdown";
import { Button } from "@repo/ui/components/button";
import { Play, Square, RotateCcw } from "lucide-react";

export const Example1 = () => {
    // 10 second countdown
    const { count, start, stop, reset, isRunning } = useCountdown(10, {
        intervalMs: 1000,
    });

    return (
        <div className="flex flex-col items-center gap-4 rounded-lg border p-6">
            <div className="font-mono text-6xl font-bold tabular-nums">
                {count.toString().padStart(2, "0")}s
            </div>

            <div className="flex gap-2">
                <Button
                    variant="outline"
                    size="icon"
                    onPress={() => start()}
                    isDisabled={isRunning || count === 0}
                    aria-label="Start"
                >
                    <Play className="h-4 w-4" />
                </Button>

                <Button
                    variant="outline"
                    size="icon"
                    onPress={stop}
                    isDisabled={!isRunning}
                    aria-label="Stop"
                >
                    <Square className="h-4 w-4 fill-current" />
                </Button>

                <Button
                    variant="outline"
                    size="icon"
                    onPress={reset}
                    aria-label="Reset"
                >
                    <RotateCcw className="h-4 w-4" />
                </Button>
            </div>
        </div>
    );
};

OTP Resend Timer

A pattern often used in authentication flows. The implementation includes an onComplete callback.

Verify your email

We sent a 6-digit code to your email. Enter it below to verify your account.

"use client";

import { useCountdown } from "@repo/hooks/utility/use-countdown";
import { Button } from "@repo/ui/components/button";
import { useEffect } from "react";

export const Example2 = () => {
    // OTP Resend Timer (30 seconds)
    const { count, start, reset, isRunning } = useCountdown(30, {
        intervalMs: 1000,
        onComplete: () => console.log("Can resend now"),
    });

    // Auto-start on mount
    useEffect(() => {
        start();
    }, [start]);

    const handleResend = async () => {
        // Simulate API call
        console.log("Resending OTP...");
        reset();
        start();
    };

    return (
        <div className="max-w-sm space-y-4 rounded-lg border p-6">
            <div className="space-y-2">
                <h3 className="font-medium">Verify your email</h3>
                <p className="text-muted-foreground text-sm">
                    We sent a 6-digit code to your email. Enter it below to
                    verify your account.
                </p>
            </div>

            <div className="flex justify-center gap-2">
                {Array.from({ length: 6 }).map((_, i) => (
                    <div
                        key={i}
                        className="bg-muted h-12 w-10 rounded-lg border text-center text-xl leading-[46px]"
                    >

                    </div>
                ))}
            </div>

            <Button
                className="w-full"
                size="sm"
                onPress={handleResend}
                isDisabled={isRunning && count > 0}
            >
                {count > 0 ? `Resend code in ${count}s` : "Resend code"}
            </Button>
        </div>
    );
};

Session Timer (Stopwatch)

Using the isIncrement option to count up, simulating a live session timer.

Session Duration
00:00:00
Stopped
"use client";

import { useCountdown } from "@repo/hooks/utility/use-countdown";
import { useEffect } from "react";

export const Example3 = () => {
    // Event Countdown: Simulate counting down 1 hour (3600s)
    // We increment to simulate a "stopwatch" or elapsed time style
    const { count, start, isRunning } = useCountdown(0, {
        isIncrement: true,
        countStop: Infinity,
    });

    useEffect(() => {
        start();
    }, [start]);

    const formatTime = (seconds: number) => {
        const h = Math.floor(seconds / 3600);
        const m = Math.floor((seconds % 3600) / 60);
        const s = seconds % 60;
        return `${h.toString().padStart(2, "0")}:${m
            .toString()
            .padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
    };

    return (
        <div className="flex flex-col items-center justify-center gap-2 rounded-lg border bg-zinc-950 p-8 text-white">
            <div className="text-xs font-medium uppercase tracking-widest text-zinc-400">
                Session Duration
            </div>
            <div className="font-mono text-5xl font-bold tabular-nums">
                {formatTime(count)}
            </div>
            <div className="flex items-center gap-2 text-sm text-zinc-500">
                <span
                    className={`h-2 w-2 rounded-full ${isRunning ? "animate-pulse bg-green-500" : "bg-red-500"}`}
                />
                {isRunning ? "Recording Live" : "Stopped"}
            </div>
        </div>
    );
};

API Reference

Hook Signature

function useCountdown(
    countStart: number,
    options?: UseCountdownOptions,
): UseCountdownReturn;

Parameters

NameTypeDescription
countStartnumberThe starting value of the countdown (in seconds/units).
optionsUseCountdownOptionsConfiguration options for the countdown.

UseCountdownOptions

NameTypeDefaultDescription
intervalMsnumber1000The interval in milliseconds.
isIncrementbooleanfalseWhether to increment (count up) instead of decrement.
countStopnumber0The value at which the countdown stops.
onComplete() => voidundefinedCallback function triggered when the countdown completes.

UseCountdownReturn

NameTypeDescription
countnumberThe current value of the countdown.
start(seconds?: number) => voidStarts the countdown. Optionally accepts a value to override the current count.
stop() => voidPauses/Stops the countdown.
reset() => voidResets the countdown to the countStart value.
isRunningbooleanWhether the countdown is currently active.

Hook Source Code

import { useState, useEffect, useRef, useCallback } from "react";

/**
 * Options for the useCountdown hook
 */
export interface UseCountdownOptions {
    /** Interval in milliseconds (default: 1000) */
    intervalMs?: number;
    /** Whether to increment instead of decrement (default: false) */
    isIncrement?: boolean;
    /** Value to stop at (default: 0) */
    countStop?: number;
    /** Callback when countdown completes */
    onComplete?: () => void;
}

/**
 * Return type for the useCountdown hook
 */
export interface UseCountdownReturn {
    /** Current count value */
    count: number;
    /** Start the countdown (optionally override duration) */
    start: (seconds?: number) => void;
    /** Stop/Pause the countdown */
    stop: () => void;
    /** Reset the countdown to initial value */
    reset: () => void;
    /** Whether the countdown is currently active */
    isRunning: boolean;
}

/**
 * A React hook for managing countdowns and timers.
 *
 * @param countStart - Starting count value (in seconds/units)
 * @param options - Configuration options for the countdown
 * @returns UseCountdownReturn object with count and control methods
 */
export function useCountdown(
    countStart: number,
    options: UseCountdownOptions = {},
): UseCountdownReturn {
    const {
        intervalMs = 1000,
        isIncrement = false,
        countStop = 0,
        onComplete,
    } = options;
    const [count, setCount] = useState(countStart);
    const [isRunning, setIsRunning] = useState(false);
    const callbackRef = useRef(onComplete);
    const timerRef = useRef<NodeJS.Timeout | null>(null);

    // Keep callback ref updated
    useEffect(() => {
        callbackRef.current = onComplete;
    }, [onComplete]);

    const stop = useCallback(() => {
        if (timerRef.current) {
            clearInterval(timerRef.current);
            timerRef.current = null;
        }
        setIsRunning(false);
    }, []);

    const start = useCallback(
        (seconds?: number) => {
            if (isRunning) return;

            if (seconds !== undefined) {
                setCount(seconds);
            }

            setIsRunning(true);
            timerRef.current = setInterval(() => {
                setCount((prevCount) => {
                    const nextCount = isIncrement
                        ? prevCount + 1
                        : prevCount - 1;

                    if (
                        isIncrement
                            ? nextCount >= countStop
                            : nextCount <= countStop
                    ) {
                        stop();
                        callbackRef.current?.();
                        return countStop;
                    }

                    return nextCount;
                });
            }, intervalMs);
        },
        [countStop, intervalMs, isIncrement, isRunning, stop],
    );

    const reset = useCallback(() => {
        stop();
        setCount(countStart);
    }, [countStart, stop]);

    // Cleanup on unmount
    useEffect(() => {
        return () => stop();
    }, [stop]);

    return {
        count,
        start,
        stop,
        reset,
        isRunning,
    };
}