Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Sleep Timer
Sleep timer dialog with duration presets, countdown display, fade-out option, and cancel functionality for media apps
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Fall asleep to your favorite content. This React sleep timer dialog provides quick duration presets, visual countdown display, optional audio fade-out, and easy cancellation for media playback. Built with shadcn/ui Dialog, Button, Badge, Switch, and Progress components using Tailwind CSS, users set automatic stop timers with satisfying visual feedback and smooth audio transitions. Select duration, enable fade, watch countdown—perfect for music players, podcast apps, audiobook readers, or any Next.js application where timed media playback helps users relax and sleep without manual intervention.
"use client";import { useState, useEffect, useRef, useCallback } from "react";import { Moon, Clock, Volume2, VolumeX, Play, X, Plus, Check, Sparkles,} from "lucide-react";import { Button } from "@/components/ui/button";import { Badge } from "@/components/ui/badge";import { Dialog, DialogContent, DialogDescription, DialogFooter, 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 { cn } from "@/lib/utils";type TimerState = "idle" | "running" | "fading" | "completed";const presets = [ { label: "15 min", minutes: 15 }, { label: "30 min", minutes: 30 }, { label: "45 min", minutes: 45 }, { label: "1 hour", minutes: 60 }, { label: "1.5 hours", minutes: 90 }, { label: "2 hours", minutes: 120 },];const extendOptions = [5, 15, 30];export const title = "React Dialog Block Sleep Timer";export default function DialogSleepTimer() { const [open, setOpen] = useState(false); const [selectedMinutes, setSelectedMinutes] = useState(30); const [remainingSeconds, setRemainingSeconds] = useState(0); const [timerState, setTimerState] = useState<TimerState>("idle"); const [fadeOutEnabled, setFadeOutEnabled] = useState(true); const [fadeOutDuration] = useState(30); // seconds const [currentVolume, setCurrentVolume] = useState(100); const intervalRef = useRef<NodeJS.Timeout | null>(null); const totalSeconds = useRef(0); const clearTimer = useCallback(() => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); const formatTime = (seconds: number) => { const hrs = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; if (hrs > 0) { return `${hrs}:${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`; } return `${mins}:${secs.toString().padStart(2, "0")}`; }; const getEndTime = () => { const endTime = new Date(Date.now() + remainingSeconds * 1000); return endTime.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); }; useEffect(() => { if (timerState === "running" || timerState === "fading") { intervalRef.current = setInterval(() => { setRemainingSeconds((prev) => { if (prev <= 1) { setTimerState("completed"); setCurrentVolume(0); console.log("Sleep timer completed - stopping playback"); return 0; } // Handle fade out if (fadeOutEnabled && prev <= fadeOutDuration && timerState !== "fading") { setTimerState("fading"); } if (timerState === "fading" || (fadeOutEnabled && prev <= fadeOutDuration)) { const fadeProgress = (fadeOutDuration - prev + 1) / fadeOutDuration; setCurrentVolume(Math.round(100 * (1 - fadeProgress))); } return prev - 1; }); }, 1000); } return clearTimer; }, [timerState, fadeOutEnabled, fadeOutDuration, clearTimer]); const handleStart = () => { totalSeconds.current = selectedMinutes * 60; setRemainingSeconds(selectedMinutes * 60); setCurrentVolume(100); setTimerState("running"); }; const handleCancel = () => { clearTimer(); setTimerState("idle"); setRemainingSeconds(0); setCurrentVolume(100); }; const handleExtend = (minutes: number) => { const additionalSeconds = minutes * 60; setRemainingSeconds((prev) => prev + additionalSeconds); totalSeconds.current += additionalSeconds; if (timerState === "fading") { setTimerState("running"); setCurrentVolume(100); } }; const handleClose = () => { setOpen(false); }; const progress = totalSeconds.current > 0 ? ((totalSeconds.current - remainingSeconds) / totalSeconds.current) * 100 : 0; const isActive = timerState === "running" || timerState === "fading"; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Moon className="mr-2 h-4 w-4" /> Sleep Timer </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-sm"> <DialogHeader> <DialogTitle className="flex items-center gap-2"> <Moon className="h-5 w-5" /> Sleep Timer </DialogTitle> <DialogDescription> Set a timer to stop playback automatically. </DialogDescription> </DialogHeader> <div className="space-y-4 py-4"> {timerState === "completed" ? ( <div className="flex flex-col items-center py-8 text-center"> <div className="rounded-full bg-primary/10 p-4 mb-4"> <Check className="h-8 w-8 text-primary" /> </div> <h3 className="text-lg font-semibold">Goodnight!</h3> <p className="text-sm text-muted-foreground mt-1"> Playback has been stopped. </p> <Button onClick={handleCancel} className="mt-4"> <Moon className="mr-2 h-4 w-4" /> Set New Timer </Button> </div> ) : isActive ? ( <> <div className="flex flex-col items-center py-4"> <div className="relative"> <div className="text-5xl font-mono font-bold tabular-nums"> {formatTime(remainingSeconds)} </div> {timerState === "fading" && ( <Badge className="absolute -top-2 -right-2 animate-pulse"> Fading </Badge> )} </div> <p className="text-sm text-muted-foreground mt-2"> Ends at {getEndTime()} </p> </div> <Progress value={progress} className="h-2" /> {fadeOutEnabled && ( <div className="flex items-center justify-between rounded-lg border p-3"> <div className="flex items-center gap-2"> {timerState === "fading" ? ( <VolumeX className="h-4 w-4 text-muted-foreground" /> ) : ( <Volume2 className="h-4 w-4 text-muted-foreground" /> )} <span className="text-sm">Volume</span> </div> <Badge variant="secondary" className="font-mono"> {currentVolume}% </Badge> </div> )} <div className="space-y-2"> <Label className="text-xs text-muted-foreground">Add more time</Label> <div className="flex gap-2"> {extendOptions.map((minutes) => ( <Button key={minutes} variant="outline" size="sm" className="flex-1" onClick={() => handleExtend(minutes)} > <Plus className="mr-1 h-3 w-3" /> {minutes}m </Button> ))} </div> </div> <Button variant="destructive" className="w-full" onClick={handleCancel} > <X className="mr-2 h-4 w-4" /> Cancel Timer </Button> </> ) : ( <> <div className="grid grid-cols-3 gap-2"> {presets.map((preset) => ( <Button key={preset.minutes} variant={selectedMinutes === preset.minutes ? "default" : "outline"} size="sm" onClick={() => setSelectedMinutes(preset.minutes)} > {preset.label} </Button> ))} </div> <div className="rounded-lg border p-4 text-center"> <div className="flex items-center justify-center gap-2 text-muted-foreground mb-1"> <Clock className="h-4 w-4" /> <span className="text-sm">Timer will end at</span> </div> <p className="text-2xl font-semibold"> {new Date(Date.now() + selectedMinutes * 60 * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", })} </p> </div> <div className="flex items-center justify-between rounded-lg border p-3"> <div className="space-y-0.5"> <Label htmlFor="fade-out" className="text-sm font-normal"> Fade out audio </Label> <p className="text-xs text-muted-foreground"> Gradually lower volume before stopping </p> </div> <Switch id="fade-out" checked={fadeOutEnabled} onCheckedChange={setFadeOutEnabled} /> </div> <div className="flex items-center gap-2 rounded-lg border p-3 text-sm text-muted-foreground"> <Sparkles className="h-4 w-4 shrink-0" /> <span>Playback will stop automatically when the timer ends.</span> </div> </> )} </div> {timerState === "idle" && ( <DialogFooter className="gap-2 sm:gap-0"> <Button variant="outline" onClick={handleClose}> Cancel </Button> <Button onClick={handleStart}> <Play className="mr-2 h-4 w-4" /> Start Timer </Button> </DialogFooter> )} </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-sleep-timer.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-sleep-timer.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-sleep-timer.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-sleep-timer.jsonRelated blocks you will also like
React Dialog Block Countdown Timer
Time tracking
React Dialog Block Pomodoro Timer
Timed sessions
React Dialog Block Notification Preferences
Alert settings
React Dialog Block Media Upload
Media management
React Dialog Block Quick Note
Bedtime notes
React Dialog Block Success Confirmation
Timer confirmation