Not affiliated with official shadcn/ui. Visit ui.shadcn.com for official docs.
Unlock this block—Get Pro at 60% offReact Dialog Block Breathing Exercise
Breathing exercise dialog with guided animations, multiple patterns, visual circle expansion, and session completion tracking
Looking to implement shadcn/ui blocks?
Join our Discord community for help from other developers.
Find your calm with guided breathing. This React breathing exercise dialog provides multiple breathing patterns (4-7-8, box breathing, relaxing), animated visual circle that expands and contracts, clear phase instructions (inhale, hold, exhale), and session completion tracking. Built with shadcn/ui Dialog, Button, Badge, Progress, and Select components using Tailwind CSS, users practice mindfulness with soothing visual guidance and satisfying animations. Select pattern, follow the circle, complete sessions—perfect for meditation apps, wellness tools, stress relief features, or any Next.js application where guided breathing helps users relax and focus.
"use client";import { useState, useEffect, useRef, useCallback } from "react";import { Wind, Play, Pause, RotateCcw, Check,} from "lucide-react";import { Button } from "@/components/ui/button";import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/components/ui/select";import { Progress } from "@/components/ui/progress";import { cn } from "@/lib/utils";type BreathingPhase = "inhale" | "hold" | "exhale" | "holdAfterExhale" | "idle";type BreathingPattern = { id: string; name: string; description: string; inhale: number; hold: number; exhale: number; holdAfterExhale: number; cycles: number;};const patterns: BreathingPattern[] = [ { id: "relaxing", name: "Relaxing Breath", description: "4-7-8 technique for calm", inhale: 4, hold: 7, exhale: 8, holdAfterExhale: 0, cycles: 4, }, { id: "box", name: "Box Breathing", description: "Equal parts for focus", inhale: 4, hold: 4, exhale: 4, holdAfterExhale: 4, cycles: 4, }, { id: "energizing", name: "Energizing Breath", description: "Quick inhale, slow exhale", inhale: 2, hold: 0, exhale: 4, holdAfterExhale: 0, cycles: 6, }, { id: "calming", name: "Deep Calm", description: "Long exhales for relaxation", inhale: 4, hold: 2, exhale: 6, holdAfterExhale: 0, cycles: 5, },];const phaseInstructions: Record<BreathingPhase, string> = { inhale: "Breathe In", hold: "Hold", exhale: "Breathe Out", holdAfterExhale: "Hold", idle: "Get Ready",};export const title = "React Dialog Block Breathing Exercise";export default function DialogBreathingExercise() { const [open, setOpen] = useState(false); const [selectedPatternId, setSelectedPatternId] = useState("relaxing"); const [isRunning, setIsRunning] = useState(false); const [phase, setPhase] = useState<BreathingPhase>("idle"); const [phaseTime, setPhaseTime] = useState(0); const [currentCycle, setCurrentCycle] = useState(0); const [isComplete, setIsComplete] = useState(false); const [sessionsCompleted, setSessionsCompleted] = useState(0); const intervalRef = useRef<NodeJS.Timeout | null>(null); const pattern = patterns.find((p) => p.id === selectedPatternId) || patterns[0]; const clearTimer = useCallback(() => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); const getPhaseProgress = () => { const phaseDuration = getCurrentPhaseDuration(); if (phaseDuration === 0) return 100; return ((phaseDuration - phaseTime) / phaseDuration) * 100; }; const getCurrentPhaseDuration = () => { switch (phase) { case "inhale": return pattern.inhale; case "hold": return pattern.hold; case "exhale": return pattern.exhale; case "holdAfterExhale": return pattern.holdAfterExhale; default: return 0; } }; const getNextPhase = (currentPhase: BreathingPhase): BreathingPhase => { switch (currentPhase) { case "inhale": return pattern.hold > 0 ? "hold" : "exhale"; case "hold": return "exhale"; case "exhale": return pattern.holdAfterExhale > 0 ? "holdAfterExhale" : "inhale"; case "holdAfterExhale": return "inhale"; default: return "inhale"; } }; const getTotalCycleTime = () => { return pattern.inhale + pattern.hold + pattern.exhale + pattern.holdAfterExhale; }; const getOverallProgress = () => { const totalTime = getTotalCycleTime() * pattern.cycles; const completedTime = currentCycle * getTotalCycleTime(); let currentPhaseOffset = 0; if (phase === "hold") currentPhaseOffset = pattern.inhale; else if (phase === "exhale") currentPhaseOffset = pattern.inhale + pattern.hold; else if (phase === "holdAfterExhale") currentPhaseOffset = pattern.inhale + pattern.hold + pattern.exhale; const elapsedInPhase = getCurrentPhaseDuration() - phaseTime; const totalElapsed = completedTime + currentPhaseOffset + elapsedInPhase; return (totalElapsed / totalTime) * 100; }; useEffect(() => { if (!isRunning) return; intervalRef.current = setInterval(() => { setPhaseTime((prev) => { if (prev <= 1) { const nextPhase = getNextPhase(phase); // Check if cycle is complete if ( (phase === "exhale" && pattern.holdAfterExhale === 0) || phase === "holdAfterExhale" ) { setCurrentCycle((c) => { const newCycle = c + 1; if (newCycle >= pattern.cycles) { setIsRunning(false); setIsComplete(true); setSessionsCompleted((s) => s + 1); return c; } return newCycle; }); } setPhase(nextPhase); switch (nextPhase) { case "inhale": return pattern.inhale; case "hold": return pattern.hold; case "exhale": return pattern.exhale; case "holdAfterExhale": return pattern.holdAfterExhale; default: return 0; } } return prev - 1; }); }, 1000); return clearTimer; }, [isRunning, phase, pattern, clearTimer]); const handleStart = () => { setPhase("inhale"); setPhaseTime(pattern.inhale); setCurrentCycle(0); setIsComplete(false); setIsRunning(true); }; const handlePause = () => { setIsRunning(false); clearTimer(); }; const handleResume = () => { setIsRunning(true); }; const handleReset = () => { clearTimer(); setIsRunning(false); setPhase("idle"); setPhaseTime(0); setCurrentCycle(0); setIsComplete(false); }; const handlePatternChange = (id: string) => { setSelectedPatternId(id); handleReset(); }; const getCircleScale = () => { if (phase === "idle") return 0.6; if (phase === "inhale") return 0.6 + (0.4 * getPhaseProgress()) / 100; if (phase === "hold") return 1; if (phase === "exhale") return 1 - (0.4 * getPhaseProgress()) / 100; if (phase === "holdAfterExhale") return 0.6; return 0.6; }; return ( <Dialog open={open} onOpenChange={setOpen}> <div className="flex min-h-[350px] items-center justify-center"> <DialogTrigger asChild> <Button variant="outline"> <Wind className="mr-2 h-4 w-4" /> Breathing Exercise </Button> </DialogTrigger> </div> <DialogContent className="sm:max-w-sm"> <DialogHeader> <DialogTitle className="flex items-center gap-2"> <Wind className="h-5 w-5" /> Breathing Exercise </DialogTitle> <DialogDescription> Follow the circle and calm your mind. </DialogDescription> </DialogHeader> <div className="space-y-6 py-4"> {isComplete ? ( <div className="flex flex-col items-center py-8 text-center"> <Check className="h-10 w-10 text-primary mb-4" /> <h3 className="text-lg font-semibold">Session Complete</h3> <p className="text-sm text-muted-foreground mt-1"> {pattern.cycles} cycles of {pattern.name.toLowerCase()} </p> <div className="flex gap-2 mt-6"> <Button variant="outline" onClick={handleReset}> Done </Button> <Button onClick={handleStart}> <Play className="mr-2 h-4 w-4" /> Repeat </Button> </div> </div> ) : ( <> {/* Pattern selector - only shown when idle */} {!isRunning && phase === "idle" && ( <div className="space-y-4"> <Select value={selectedPatternId} onValueChange={handlePatternChange}> <SelectTrigger> <SelectValue /> </SelectTrigger> <SelectContent> {patterns.map((p) => ( <SelectItem key={p.id} value={p.id}> {p.name} — {p.description} </SelectItem> ))} </SelectContent> </Select> <p className="text-sm text-muted-foreground text-center"> {pattern.inhale}s in {pattern.hold > 0 && ` · ${pattern.hold}s hold`} {` · ${pattern.exhale}s out`} {pattern.holdAfterExhale > 0 && ` · ${pattern.holdAfterExhale}s hold`} {` · ${pattern.cycles} cycles`} </p> </div> )} {/* Breathing circle */} <div className="flex justify-center items-center h-48"> <div className={cn( "flex items-center justify-center rounded-full border-2 border-foreground/20 transition-all", isRunning && "duration-1000 ease-in-out" )} style={{ width: `${120 + 60 * getCircleScale()}px`, height: `${120 + 60 * getCircleScale()}px`, }} > <div className="text-center"> <p className="text-base font-medium text-muted-foreground"> {phaseInstructions[phase]} </p> {phase !== "idle" && ( <p className="text-4xl font-semibold tabular-nums"> {phaseTime} </p> )} </div> </div> </div> {/* Progress - only shown during exercise */} {(isRunning || phase !== "idle") && ( <div className="space-y-2"> <Progress value={getOverallProgress()} className="h-1.5" /> <p className="text-sm text-muted-foreground text-center"> Cycle {currentCycle + 1} of {pattern.cycles} </p> </div> )} {/* Controls */} <div className="flex justify-center gap-2"> {phase === "idle" ? ( <Button onClick={handleStart} size="lg"> <Play className="mr-2 h-4 w-4" /> Begin </Button> ) : isRunning ? ( <> <Button variant="outline" onClick={handlePause}> <Pause className="mr-2 h-4 w-4" /> Pause </Button> <Button variant="outline" onClick={handleReset}> <RotateCcw className="mr-2 h-4 w-4" /> Stop </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" /> Stop </Button> </> )} </div> </> )} {sessionsCompleted > 0 && ( <p className="text-xs text-center text-muted-foreground"> {sessionsCompleted} session{sessionsCompleted !== 1 ? "s" : ""} completed </p> )} </div> </DialogContent> </Dialog> );}Installation
npx shadcn@latest add https://www.shadcn.io/registry/dialog-breathing-exercise.jsonnpx shadcn@latest add https://www.shadcn.io/registry/dialog-breathing-exercise.jsonpnpm dlx shadcn@latest add https://www.shadcn.io/registry/dialog-breathing-exercise.jsonbunx shadcn@latest add https://www.shadcn.io/registry/dialog-breathing-exercise.jsonRelated blocks you will also like
React Dialog Block Pomodoro Timer
Focus sessions
React Dialog Block Sleep Timer
Relaxation
React Dialog Block Countdown Timer
Timed exercises
React Dialog Block Habit Tracker
Wellness habits
React Dialog Block Success Confirmation
Session complete
React Dialog Block Notification Preferences
Reminder settings
Questions you might have
React Dialog Block Bookmark Folder
Bookmark save dialog with folder selection, new folder creation, tag input, and notes field for organizing saved items
React Dialog Block Budget Allocator
Budget allocator dialog with category sliders, visual breakdown chart, remaining balance tracker, and preset templates