Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Countdown Timer
Countdown timer dialog with custom duration input, visual progress ring, pause/resume controls, and completion alert
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Count down to any moment with precision. This React countdown timer dialog provides custom duration input for hours, minutes, and seconds, a visual progress ring showing remaining time, pause and resume controls, and completion alerts with optional sound. Built with shadcn/ui Dialog, Input, Button, and Badge components using Tailwind CSS, users set countdowns for any purpose with clear visual feedback. Set duration, start timer, track progress—perfect for cooking timers, meeting countdowns, workout intervals, or any Next.js application where precise time tracking with visual feedback helps users manage time-sensitive activities.
"use client";import { useState, useEffect, useRef, useCallback } from "react";import { Timer, Play, Pause, RotateCcw, Plus, Bell, BellOff, Check,} from "lucide-react";import { Button } from "@/components/ui/button";import { Badge } from "@/components/ui/badge";import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { Input } from "@/components/ui/input";import { Label } from "@/components/ui/label";import { cn } from "@/lib/utils";const presets = [ { label: "1 min", seconds: 60 }, { label: "5 min", seconds: 300 }, { label: "10 min", seconds: 600 }, { label: "15 min", seconds: 900 }, { label: "30 min", seconds: 1800 }, { label: "1 hour", seconds: 3600 },];export const title = "React Dialog Block Countdown Timer";export default function DialogCountdownTimer() { const [open, setOpen] = useState(false); const [hours, setHours] = useState("0"); const [minutes, setMinutes] = useState("5"); const [seconds, setSeconds] = useState("0"); const [totalSeconds, setTotalSeconds] = useState(0); const [remainingSeconds, setRemainingSeconds] = useState(0); const [isRunning, setIsRunning] = useState(false); const [isComplete, setIsComplete] = useState(false); const [soundEnabled, setSoundEnabled] = useState(true); const intervalRef = useRef<NodeJS.Timeout | null>(null); const clearTimer = useCallback(() => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); const calculateTotalSeconds = useCallback(() => { const h = parseInt(hours) || 0; const m = parseInt(minutes) || 0; const s = parseInt(seconds) || 0; return h * 3600 + m * 60 + s; }, [hours, minutes, seconds]); const formatTime = (totalSecs: number) => { const h = Math.floor(totalSecs / 3600); const m = Math.floor((totalSecs % 3600) / 60); const s = totalSecs % 60; if (h > 0) { return h.toString().padStart(2, "0") + ":" + m.toString().padStart(2, "0") + ":" + s.toString().padStart(2, "0"); } return m.toString().padStart(2, "0") + ":" + s.toString().padStart(2, "0"); }; const getProgress = () => { if (totalSeconds === 0) return 0; return ((totalSeconds - remainingSeconds) / totalSeconds) * 100; }; const circumference = 2 * Math.PI * 45; const strokeDashoffset = circumference - (getProgress() / 100) * circumference; useEffect(() => { if (!isRunning || remainingSeconds <= 0) return; intervalRef.current = setInterval(() => { setRemainingSeconds((prev) => { if (prev <= 1) { clearTimer(); setIsRunning(false); setIsComplete(true); return 0; } return prev - 1; }); }, 1000); return clearTimer; }, [isRunning, clearTimer]); const handleStart = () => { const total = calculateTotalSeconds(); if (total <= 0) return; setTotalSeconds(total); setRemainingSeconds(total); setIsRunning(true); setIsComplete(false); }; const handlePause = () => { setIsRunning(false); clearTimer(); }; const handleResume = () => { if (remainingSeconds > 0) { setIsRunning(true); } }; const handleReset = () => { clearTimer(); setIsRunning(false); setIsComplete(false); setRemainingSeconds(0); setTotalSeconds(0); }; const handlePreset = (secs: number) => { const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); const s = secs % 60; setHours(h.toString()); setMinutes(m.toString()); setSeconds(s.toString()); }; const handleAddTime = (secs: number) => { setRemainingSeconds((prev) => prev + secs); setTotalSeconds((prev) => prev + secs); }; const handleDismissComplete = () => { setIsComplete(false); setRemainingSeconds(0); setTotalSeconds(0); }; const isTimerSet = remainingSeconds > 0 || isRunning; const isLowTime = remainingSeconds <= 10 && remainingSeconds > 0; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Timer className="mr-2 h-4 w-4" /> Countdown Timer </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-sm"> <DialogHeader> <DialogTitle className="flex items-center justify-between"> <div className="flex items-center gap-2"> <Timer className="h-5 w-5" /> Countdown Timer </div> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setSoundEnabled(!soundEnabled)} title={soundEnabled ? "Sound on" : "Sound off"} > {soundEnabled ? ( <Bell className="h-4 w-4" /> ) : ( <BellOff className="h-4 w-4" /> )} </Button> </DialogTitle> <DialogDescription> Set a countdown for any duration. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> {isComplete ? ( <div className="flex flex-col items-center py-6 text-center"> <div className="rounded-full border-4 border-primary p-4 animate-pulse"> <Check className="h-8 w-8 text-primary" /> </div> <h3 className="mt-4 text-lg font-semibold">Time's Up!</h3> <p className="mt-1 text-sm text-muted-foreground"> Your countdown has finished. </p> <div className="flex gap-2 mt-4"> <Button variant="outline" onClick={handleDismissComplete}> <RotateCcw className="mr-2 h-4 w-4" /> New Timer </Button> <Button onClick={() => { handleDismissComplete(); handleStart(); }}> <Play className="mr-2 h-4 w-4" /> Restart </Button> </div> </div> ) : isTimerSet ? ( <> <div className="flex justify-center"> <div className="relative w-32 h-32"> <svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100"> <circle cx="50" cy="50" r="45" stroke="currentColor" strokeWidth="6" fill="none" className="text-muted" /> <circle cx="50" cy="50" r="45" stroke="currentColor" strokeWidth="6" fill="none" strokeLinecap="round" className={cn( "transition-all duration-1000", isLowTime ? "text-destructive" : "text-primary" )} style={{ strokeDasharray: circumference, strokeDashoffset, }} /> </svg> <div className="absolute inset-0 flex items-center justify-center"> <span className={cn( "text-lg font-mono font-bold tabular-nums", isLowTime && "text-destructive animate-pulse" )} > {formatTime(remainingSeconds)} </span> </div> </div> </div> <div className="flex justify-center gap-2"> {isRunning ? ( <Button variant="outline" onClick={handlePause}> <Pause className="mr-2 h-4 w-4" /> Pause </Button> ) : ( <Button onClick={handleResume}> <Play className="mr-2 h-4 w-4" /> Resume </Button> )} <Button variant="outline" onClick={handleReset}> <RotateCcw className="mr-2 h-4 w-4" /> Reset </Button> </div> <div className="flex justify-center gap-2"> <Button variant="outline" size="sm" onClick={() => handleAddTime(60)} > <Plus className="mr-1 h-3 w-3" /> 1 min </Button> <Button variant="outline" size="sm" onClick={() => handleAddTime(300)} > <Plus className="mr-1 h-3 w-3" /> 5 min </Button> </div> </> ) : ( <> <div className="grid grid-cols-3 gap-3"> <div className="space-y-2"> <Label htmlFor="hours" className="text-xs text-center block"> Hours </Label> <Input id="hours" type="number" min="0" max="23" value={hours} onChange={(e) => setHours(e.target.value)} className="text-center font-mono text-lg" /> </div> <div className="space-y-2"> <Label htmlFor="minutes" className="text-xs text-center block"> Minutes </Label> <Input id="minutes" type="number" min="0" max="59" value={minutes} onChange={(e) => setMinutes(e.target.value)} className="text-center font-mono text-lg" /> </div> <div className="space-y-2"> <Label htmlFor="seconds" className="text-xs text-center block"> Seconds </Label> <Input id="seconds" type="number" min="0" max="59" value={seconds} onChange={(e) => setSeconds(e.target.value)} className="text-center font-mono text-lg" /> </div> </div> <div className="space-y-2"> <Label className="text-xs text-muted-foreground">Quick presets</Label> <div className="grid grid-cols-3 gap-2"> {presets.map((preset) => ( <Button key={preset.label} variant="outline" size="sm" onClick={() => handlePreset(preset.seconds)} > {preset.label} </Button> ))} </div> </div> <Button className="w-full" size="lg" onClick={handleStart} disabled={calculateTotalSeconds() <= 0} > <Play className="mr-2 h-5 w-5" /> Start Countdown </Button> {calculateTotalSeconds() > 0 && ( <p className="text-sm text-center text-muted-foreground"> Total: {formatTime(calculateTotalSeconds())} </p> )} </> )} </div> </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-countdown-timer.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-countdown-timer.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-countdown-timer.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-countdown-timer.jsonRelated blocks you will also like
React Dialog Block Pomodoro Timer
Work sessions
React Dialog Block Sleep Timer
Auto-off timer
React Dialog Block Breathing Exercise
Timed exercises
React Dialog Block Quick Calculator
Quick tools
React Dialog Block Success Confirmation
Timer complete
React Dialog Block Notification Preferences
Alert settings
Questions you might have
React Dialog Block Copy To Clipboard
Copy to clipboard dialog with multiple content types, code syntax highlighting, one-click copy, and success feedback
React Dialog Block Create Workspace
Create workspace form dialog with input field, switch toggle for privacy setting, and sectioned footer background