Breathing Animation — Free HTML CSS JS Snippet
Breathing / Focus Ring · Loaders · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="scene">
<div class="breathing-wrap" id="wrap">
<div class="ring r4"></div>
<div class="ring r3"></div>
<div class="ring r2"></div>
<div class="ring r1"></div>
<div class="core">
<div class="label" id="label">Breathe in</div>
<div class="timer" id="timer">4</div>
</div>
</div>
<div class="controls">
<button class="btn active" id="start-btn" onclick="toggle()">Pause</button>
<select class="sel" onchange="setPattern(this.value)">
<option value="4-4">4-4 Box</option>
<option value="4-7-8">4-7-8</option>
<option value="5-5">5-5 Equal</option>
</select>
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #050810; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.scene { display: flex; flex-direction: column; align-items: center; gap: 36px; }
.breathing-wrap { position: relative; width: 240px; height: 240px; display: flex; align-items: center; justify-content: center; }
.ring {
position: absolute; border-radius: 50%;
animation: breathe 8s ease-in-out infinite;
}
.r1 { width: 80px; height: 80px; background: rgba(99,102,241,0.6); animation-delay: 0s; filter: blur(0); }
.r2 { width: 120px; height: 120px; background: rgba(99,102,241,0.25); animation-delay: 0.2s; filter: blur(2px); }
.r3 { width: 170px; height: 170px; background: rgba(99,102,241,0.12); animation-delay: 0.4s; filter: blur(4px); }
.r4 { width: 220px; height: 220px; background: rgba(99,102,241,0.06); animation-delay: 0.6s; filter: blur(8px); }
@keyframes breathe {
0%,100% { transform: scale(0.8); opacity: 0.7; }
50% { transform: scale(1.1); opacity: 1; }
}
.core { position: relative; z-index: 1; text-align: center; display: flex; flex-direction: column; gap: 4px; }
.label { font-size: 13px; font-weight: 600; color: rgba(165,180,252,0.8); letter-spacing: 1px; text-transform: uppercase; }
.timer { font-size: 48px; font-weight: 800; color: #f1f5f9; line-height: 1; font-family: 'Courier New', monospace; }
.controls { display: flex; gap: 10px; align-items: center; }
.btn { padding: 8px 20px; font-size: 13px; font-weight: 600; background: #1e293b; color: #64748b; border: 1px solid #334155; border-radius: 8px; cursor: pointer; font-family: inherit; transition: all 0.15s; }
.btn.active { background: #6366f1; color: #fff; border-color: #6366f1; }
.sel { padding: 8px 12px; font-size: 13px; font-family: inherit; background: #1e293b; color: #64748b; border: 1px solid #334155; border-radius: 8px; outline: none; cursor: pointer; }let patterns = {
'4-4': [{label:'Breathe in',dur:4},{label:'Breathe out',dur:4}],
'4-7-8': [{label:'Breathe in',dur:4},{label:'Hold',dur:7},{label:'Breathe out',dur:8}],
'5-5': [{label:'Breathe in',dur:5},{label:'Breathe out',dur:5}],
};
let pat = patterns['4-4'], phase = 0, remaining = pat[0].dur, running = true;
let timer = null;
function setPattern(key) { pat = patterns[key]; phase = 0; remaining = pat[0].dur; updateUI(); }
function updateUI() {
document.getElementById('label').textContent = pat[phase].label;
document.getElementById('timer').textContent = remaining;
const isIn = pat[phase].label.includes('in');
const isHold = pat[phase].label.includes('Hold');
const dur = (pat[phase].dur * 2) + 's';
document.querySelectorAll('.ring').forEach((r, i) => {
r.style.animationDuration = dur;
r.style.animationPlayState = running ? 'running' : 'paused';
if (isHold) { r.style.animationPlayState = 'paused'; }
});
}
function tick() {
remaining--;
if (remaining <= 0) {
phase = (phase + 1) % pat.length;
remaining = pat[phase].dur;
}
updateUI();
}
function toggle() {
running = !running;
const btn = document.getElementById('start-btn');
btn.textContent = running ? 'Pause' : 'Resume';
btn.classList.toggle('active', running);
if (running) { timer = setInterval(tick, 1000); }
else { clearInterval(timer); }
updateUI();
}
timer = setInterval(tick, 1000);
updateUI();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Breathing Animation — Guided Patterns, CSS scale Pulse & Phase Timer Countdown

A guided breathing animation is a visual tool for practicing breathing techniques that reduce anxiety, improve focus, and aid in sleep. The visual expanding and contracting circle guides the user through each phase — inhale, hold, exhale — in sync with a countdown timer, so the user can follow along without counting mentally. This snippet implements three clinically recognised breathing patterns using data-driven animation: box breathing (4-4), the 4-7-8 technique, and equal breathing (5-5).
How the breathing pattern data structure works
Each breathing pattern is stored as an array of { label, dur } phase objects in the patterns object. The 4-4 pattern has two phases: [{label:'Breathe in', dur:4}, {label:'Breathe out', dur:4}]. The 4-7-8 pattern adds a hold phase: inhale 4s, hold 7s, exhale 8s. The 5-5 pattern is simple equal breathing: in 5s, out 5s. The setPattern(key) function resets to the first phase and duration when the user switches patterns.
The CSS animation tied to breathing phases
Four .ring divs of increasing size and decreasing opacity create the layered glow rings around the central core. All rings share the same @keyframes breathe animation: 0%,100% { transform: scale(0.8); opacity: 0.7 } to 50% { transform: scale(1.1); opacity: 1 }. The animation duration is dynamically set in updateUI() via r.style.animationDuration = dur, where dur = (pat[phase].dur * 2) + 's' — doubling the phase duration makes the animation complete exactly one full scale cycle per inhale-exhale pair.
Hold state pausing
During a hold phase, r.style.animationPlayState = 'paused' freezes the rings at their current scale position, visually communicating "stay expanded" during the hold. On inhale and exhale phases, animationPlayState is set back to 'running'.
The phase countdown timer
setInterval(tick, 1000) decrements remaining each second. When it reaches zero, the phase index increments (wrapping via modulo) and remaining resets to the new phase's duration. The updateUI() function then updates both the label text and countdown number displayed inside the core element.
Pause and resume controls
The toggle() function pauses and resumes the session by clearing or restarting the setInterval and toggling the ring's animationPlayState. This lets users pause mid-session if interrupted without losing their place in the pattern.
Step by step
How to Use
- 1Observe the breathing animation running by defaultThe animation starts automatically on the 4-4 box breathing pattern. Watch the layered rings expand during the Breathe in phase (countdown 4,3,2,1) and contract during the Breathe out phase. The Pause button lets you stop mid-session.
- 2Switch between breathing patterns using the dropdownUse the dropdown selector to change patterns: 4-4 Box Breathing for general stress relief, 4-7-8 (longer exhale, clinically studied for anxiety reduction), and 5-5 Equal Breathing for balanced calm. The animation duration and phase labels update immediately on change.
- 3Add a custom breathing pattern to the patterns objectIn the JS panel, add a new key to the patterns object: "6-2-6": [{label:"Breathe in",dur:6},{label:"Hold",dur:2},{label:"Breathe out",dur:6}]. Then add a matching option in the HTML select element: <option value="6-2-6">6-2-6 Extended</option>.
- 4Change the ring colours and glow to match your app themeIn the CSS panel, update the rgba(99,102,241,...) values in .r1 through .r4 backgrounds to your accent colour. Adjust the blur() values to control the softness of the outer glow rings. The inner .r1 has no blur for a sharp center, while .r4 has blur(8px) for the outermost ambient glow.
- 5Add a session duration limiterTrack completed cycles in a counter variable. After N cycles (e.g. 5 complete breath cycles), call the toggle() function to stop the session and show a completion message. Each cycle is one full pass through all phases in the pattern array.
- 6Export as a standalone breathing exercise page or React componentClick HTML for a self-contained breathing exercise page, JSX for a React component managing phase state and animation via useEffect and useState, or Tailwind for a styled React version ready for integration into a wellness or productivity application.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Each pattern is an array of { label, dur } phase objects stored in the patterns object. When a pattern is selected via setPattern(key), the pat variable is set to the pattern array and phase resets to 0. The setInterval(tick, 1000) decrements the remaining counter each second. When remaining reaches zero, phase increments by 1 and wraps back to 0 via modulo when it reaches pat.length. The updateUI() function then reads pat[phase].dur to set the new remaining countdown and pat[phase].label to update the instruction text. The animation duration is set as (pat[phase].dur * 2) + 's' — doubling the phase duration aligns the CSS scale animation peak with the midpoint of the breathe-in duration.
The @keyframes breathe animation is applied in CSS with a static duration. In updateUI(), the duration is overridden at runtime using element.style.animationDuration = dur where dur equals the doubled phase duration as a string like '8s'. Inline styles in CSS cascade above class-based declarations, so this override takes effect immediately without removing or re-adding the animation class. Setting animationPlayState to 'paused' during hold phases freezes the rings at their peak scale, visually communicating the hold state.
Add a new entry to the patterns object: patterns['6-2-6'] = [{label:'Breathe in',dur:6},{label:'Hold',dur:2},{label:'Breathe out',dur:6}]. Then add a matching option in the HTML select element: <option value='6-2-6'>6-2-6 Extended</option>. The setPattern() function reads the select value and looks up the pattern by key, so no other JS changes are needed. You can also add entirely new phase labels — any string works as the label.
Use the Web Audio API to generate a brief tone at each phase transition. In updateUI(), create a tone when the phase changes: const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = pat[phase].label.includes('in') ? 440 : pat[phase].label.includes('Hold') ? 528 : 220; gain.gain.setValueAtTime(0.1, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.3). Different frequencies for each phase provide distinct audio cues: high for inhale, mid for hold, low for exhale.