Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Pomodoro Timer
Pomodoro timer dialog with work/break cycles, session counter, customizable durations, and sound notification toggle
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Master your focus with the Pomodoro technique. This React Pomodoro timer dialog provides configurable work and break intervals, automatic cycle transitions, session completion tracking, and optional sound notifications. Built with shadcn/ui Dialog, Button, Badge, Progress, and Switch components using Tailwind CSS, users maintain productivity with visual countdown feedback and session progress. Start timer, take breaks, track sessions—perfect for productivity apps, study tools, task managers, or any Next.js application where time-boxed focus sessions improve work efficiency and prevent burnout.
"use client";import { useState, useEffect, useRef, useCallback } from "react";import { Clock, Play, Pause, SkipForward, RotateCcw, Coffee, Brain, Volume2, VolumeX, Settings, Trophy,} 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 { Label } from "@/components/ui/label";import { Switch } from "@/components/ui/switch";import { Progress } from "@/components/ui/progress";import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select";import { cn } from "@/lib/utils";type TimerMode = "work" | "shortBreak" | "longBreak";type TimerState = "idle" | "running" | "paused" | "completed";const defaultDurations = { work: 25 * 60, shortBreak: 5 * 60, longBreak: 15 * 60,};const durationOptions = { work: [15, 20, 25, 30, 45, 50], shortBreak: [3, 5, 10], longBreak: [15, 20, 30],};export const title = "React Dialog Block Pomodoro Timer";export default function DialogPomodoroTimer() { const [open, setOpen] = useState(false); const [mode, setMode] = useState<TimerMode>("work"); const [timerState, setTimerState] = useState<TimerState>("idle"); const [durations, setDurations] = useState(defaultDurations); const [remainingSeconds, setRemainingSeconds] = useState(defaultDurations.work); const [completedSessions, setCompletedSessions] = useState(0); const [soundEnabled, setSoundEnabled] = useState(true); const [autoStartBreaks, setAutoStartBreaks] = useState(true); const [showSettings, setShowSettings] = useState(false); const intervalRef = useRef<NodeJS.Timeout | null>(null); const totalSeconds = durations[mode]; const progress = ((totalSeconds - remainingSeconds) / totalSeconds) * 100; const sessionsUntilLongBreak = 4 - (completedSessions % 4); const clearTimer = useCallback(() => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); const playSound = useCallback(() => { if (soundEnabled) { console.log("Playing notification sound..."); } }, [soundEnabled]); const switchMode = useCallback((newMode: TimerMode, autoStart = false) => { clearTimer(); setMode(newMode); setRemainingSeconds(durations[newMode]); setTimerState(autoStart ? "running" : "idle"); }, [clearTimer, durations]); const handleSessionComplete = useCallback(() => { playSound(); clearTimer(); if (mode === "work") { const newCompletedSessions = completedSessions + 1; setCompletedSessions(newCompletedSessions); const nextMode = newCompletedSessions % 4 === 0 ? "longBreak" : "shortBreak"; switchMode(nextMode, autoStartBreaks); } else { switchMode("work", false); } }, [mode, completedSessions, autoStartBreaks, playSound, clearTimer, switchMode]); useEffect(() => { if (timerState === "running" && remainingSeconds > 0) { intervalRef.current = setInterval(() => { setRemainingSeconds((prev) => { if (prev <= 1) { handleSessionComplete(); return 0; } return prev - 1; }); }, 1000); } return clearTimer; }, [timerState, handleSessionComplete, clearTimer]); const formatTime = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; }; const handleStart = () => { if (remainingSeconds > 0) { setTimerState("running"); } }; const handlePause = () => { clearTimer(); setTimerState("paused"); }; const handleReset = () => { clearTimer(); setRemainingSeconds(durations[mode]); setTimerState("idle"); }; const handleSkip = () => { if (mode === "work") { const nextMode = (completedSessions + 1) % 4 === 0 ? "longBreak" : "shortBreak"; switchMode(nextMode, false); } else { switchMode("work", false); } }; const handleModeChange = (newMode: TimerMode) => { if (timerState === "running") { clearTimer(); } switchMode(newMode, false); }; const handleDurationChange = (modeKey: TimerMode, minutes: string) => { const seconds = parseInt(minutes) * 60; setDurations((prev) => ({ ...prev, [modeKey]: seconds })); if (mode === modeKey && timerState === "idle") { setRemainingSeconds(seconds); } }; const getModeColor = () => { switch (mode) { case "work": return "text-primary"; case "shortBreak": return "text-primary/80"; case "longBreak": return "text-primary/60"; } }; const getModeIcon = () => { switch (mode) { case "work": return <Brain className="h-5 w-5" />; case "shortBreak": case "longBreak": return <Coffee className="h-5 w-5" />; } }; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Clock className="mr-2 h-4 w-4" /> Pomodoro 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"> <Clock className="h-5 w-5" /> Pomodoro Timer </div> <Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => setShowSettings(!showSettings)} > <Settings className="h-4 w-4" /> </Button> </DialogTitle> <DialogDescription> Focus with timed work sessions and breaks. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> {showSettings ? ( <div className="space-y-4"> <div className="space-y-3"> <Label className="text-sm font-medium">Durations</Label> <div className="grid gap-3"> <div className="flex items-center justify-between"> <Label className="text-sm font-normal">Work</Label> <Select value={(durations.work / 60).toString()} onValueChange={(v) => handleDurationChange("work", v)} > <SelectTrigger className="w-24"> <SelectValue /> </SelectTrigger> <SelectContent> {durationOptions.work.map((min) => ( <SelectItem key={min} value={min.toString()}> {min} min </SelectItem> ))} </SelectContent> </Select> </div> <div className="flex items-center justify-between"> <Label className="text-sm font-normal">Short Break</Label> <Select value={(durations.shortBreak / 60).toString()} onValueChange={(v) => handleDurationChange("shortBreak", v)} > <SelectTrigger className="w-24"> <SelectValue /> </SelectTrigger> <SelectContent> {durationOptions.shortBreak.map((min) => ( <SelectItem key={min} value={min.toString()}> {min} min </SelectItem> ))} </SelectContent> </Select> </div> <div className="flex items-center justify-between"> <Label className="text-sm font-normal">Long Break</Label> <Select value={(durations.longBreak / 60).toString()} onValueChange={(v) => handleDurationChange("longBreak", v)} > <SelectTrigger className="w-24"> <SelectValue /> </SelectTrigger> <SelectContent> {durationOptions.longBreak.map((min) => ( <SelectItem key={min} value={min.toString()}> {min} min </SelectItem> ))} </SelectContent> </Select> </div> </div> </div> <div className="space-y-3"> <Label className="text-sm font-medium">Preferences</Label> <div className="space-y-2"> <div className="flex items-center justify-between"> <Label htmlFor="auto-breaks" className="text-sm font-normal"> Auto-start breaks </Label> <Switch id="auto-breaks" checked={autoStartBreaks} onCheckedChange={setAutoStartBreaks} /> </div> <div className="flex items-center justify-between"> <Label htmlFor="sound" className="text-sm font-normal"> Sound notifications </Label> <Switch id="sound" checked={soundEnabled} onCheckedChange={setSoundEnabled} /> </div> </div> </div> <Button variant="outline" className="w-full" onClick={() => setShowSettings(false)} > Done </Button> </div> ) : ( <> <div className="flex justify-center gap-2"> {(["work", "shortBreak", "longBreak"] as TimerMode[]).map((m) => ( <Button key={m} variant={mode === m ? "default" : "outline"} size="sm" onClick={() => handleModeChange(m)} disabled={timerState === "running"} > {m === "work" ? "Work" : m === "shortBreak" ? "Short" : "Long"} </Button> ))} </div> <div className="flex flex-col items-center py-6"> <div className={cn("flex items-center gap-2 mb-2", getModeColor())}> {getModeIcon()} <span className="text-sm font-medium capitalize"> {mode === "shortBreak" ? "Short Break" : mode === "longBreak" ? "Long Break" : "Focus Time"} </span> </div> <span className={cn( "text-6xl font-mono font-bold tabular-nums", timerState === "running" && getModeColor() )} > {formatTime(remainingSeconds)} </span> <Progress value={progress} className="w-full mt-4 h-2" /> </div> <div className="flex justify-center gap-2"> {timerState === "running" ? ( <Button onClick={handlePause} variant="outline" size="lg"> <Pause className="mr-2 h-5 w-5" /> Pause </Button> ) : ( <Button onClick={handleStart} size="lg"> <Play className="mr-2 h-5 w-5" /> {timerState === "paused" ? "Resume" : "Start"} </Button> )} <Button onClick={handleReset} variant="outline" size="icon" className="h-11 w-11"> <RotateCcw className="h-5 w-5" /> </Button> <Button onClick={handleSkip} variant="outline" size="icon" className="h-11 w-11"> <SkipForward className="h-5 w-5" /> </Button> </div> <div className="flex items-center justify-between rounded-lg border p-3"> <div className="flex items-center gap-2"> <Trophy className="h-4 w-4 text-muted-foreground" /> <span className="text-sm">Sessions completed</span> </div> <Badge variant="secondary" className="font-mono"> {completedSessions} </Badge> </div> {mode === "work" && ( <p className="text-center text-xs text-muted-foreground"> {sessionsUntilLongBreak === 4 ? "Long break after this session" : `${sessionsUntilLongBreak} session${sessionsUntilLongBreak > 1 ? "s" : ""} until long break`} </p> )} <div className="flex justify-center"> <Button variant="ghost" size="sm" onClick={() => setSoundEnabled(!soundEnabled)} className="text-muted-foreground" > {soundEnabled ? ( <Volume2 className="mr-2 h-4 w-4" /> ) : ( <VolumeX className="mr-2 h-4 w-4" /> )} {soundEnabled ? "Sound on" : "Sound off"} </Button> </div> </> )} </div> </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-pomodoro-timer.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-pomodoro-timer.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-pomodoro-timer.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-pomodoro-timer.jsonRelated blocks you will also like
React Dialog Block Countdown Timer
General timer
React Dialog Block Schedule Meeting
Time management
React Dialog Block Content Scheduler
Task scheduling
React Dialog Block Notification Preferences
Alert settings
React Dialog Block Goal Setting
Productivity goals
React Dialog Block Quick Note
Session notes
Questions you might have
React Dialog Block Poll Create
Poll creation dialog with question input, multiple choice options, voting settings, duration picker, and preview
React Dialog Block Price Alert
Price alert dialog with target price input, comparison operators, notification channel selection, and expiration date for price monitoring