Square Breathing

6 second breathing metronome Cycle 0
6

Ready

Inhale • pause • exhale • pause

Inhale
6 sec
Pause
6 sec
Exhale
6 sec
Pause
6 sec
Seconds per phase 6 sec Volume 65% Meditative female voice

Press Start to begin.

#breath-app { font-family: system-ui, -apple-system, BlinkMacSystemFont, “Segoe UI”, sans-serif; color: #f6f8fb; background: #111418; padding: 24px; } #breath-app .breath-card { max-width: 720px; margin: auto; background: #1b2026; border: 1px solid rgba(255,255,255,.1); border-radius: 10px; padding: 28px; text-align: center; } #breath-app .top { display: flex; justify-content: space-between; color: #aeb8c2; margin-bottom: 28px; } #breath-app .orb { width: min(320px, 72vw); aspect-ratio: 1; margin: 0 auto 24px; border-radius: 50%; display: grid; place-items: center; background: radial-gradient(circle, #35d19a, #3385d6); box-shadow: 0 0 0 16px rgba(53,209,154,.22), 0 0 60px rgba(53,209,154,.28); transform: scale(.72); transition: transform .2s linear; } #breath-app #timer { font-size: 72px; font-weight: 800; background: rgba(0,0,0,.28); width: 46%; aspect-ratio: 1; border-radius: 50%; display: grid; place-items: center; } #breath-app h1 { font-size: clamp(44px, 9vw, 76px); margin: 0; } #breath-app #hint { color: #aeb8c2; margin: 8px 0 22px; } #breath-app .steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 22px; } #breath-app .steps div { background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.08); border-radius: 8px; padding: 12px; font-weight: 700; } #breath-app .steps div.active { border-color: #35d19a; background: rgba(53,209,154,.16); } #breath-app label { display: grid; gap: 8px; text-align: left; margin: 14px 0; color: #aeb8c2; } #breath-app input[type=”range”], #breath-app input[type=”checkbox”] { accent-color: #35d19a; } #breath-app .voice { display: flex; justify-content: space-between; align-items: center; } #breath-app button { min-height: 48px; border: 0; border-radius: 8px; padding: 0 22px; margin: 8px 4px; font-weight: 800; cursor: pointer; } #breath-app #start { background: #35d19a; color: #06110c; } #breath-app #reset { background: #2a3037; color: #f6f8fb; } #breath-app #status { color: #aeb8c2; } @media (max-width: 560px) { #breath-app .steps { grid-template-columns: repeat(2, 1fr); } } (() => { const phases = [ { key: “inhale”, title: “Inhale”, hint: “Breathe in smoothly”, word: “inhale”, tone: 660 }, { key: “pause-in”, title: “Pause”, hint: “Hold gently”, word: “pause”, tone: 520 }, { key: “exhale”, title: “Exhale”, hint: “Release slowly”, word: “exhale”, tone: 440 }, { key: “pause-out”, title: “Pause”, hint: “Rest before the next breath”, word: “pause”, tone: 360 } ]; const $ = (id) => document.getElementById(id); const timer = $(“timer”), phase = $(“phase”), hint = $(“hint”), orb = $(“orb”); const seconds = $(“seconds”), secondsText = $(“secondsText”); const volume = $(“volume”), volumeText = $(“volumeText”); const start = $(“start”), reset = $(“reset”), status = $(“status”); const voiceToggle = $(“voice”), cycle = $(“cycle”); const steps = […document.querySelectorAll(“#breath-app [data-step]”)]; let running = false, index = 0, startedAt = 0, raf = 0, cycles = 0, audio, chosenVoice; function pickVoice() { if (!speechSynthesis) return null; const voices = speechSynthesis.getVoices(); return voices.find(v => /female|woman|aria|jenny|samantha|victoria|karen|zira/i.test(v.name)) || voices.find(v => v.lang && v.lang.startsWith(“en”)) || voices[0]; } function speak(text) { if (!voiceToggle.checked || !(“speechSynthesis” in window)) return; chosenVoice = chosenVoice || pickVoice(); const u = new SpeechSynthesisUtterance(text); if (chosenVoice) u.voice = chosenVoice; u.rate = 0.72; u.pitch = 1.08; u.volume = Math.max(0.35, Number(volume.value) / 100); speechSynthesis.cancel(); speechSynthesis.speak(u); } function beep(freq) { const AudioEngine = window.AudioContext || window.webkitAudioContext; if (!AudioEngine || Number(volume.value) === 0) return; audio = audio || new AudioEngine(); const osc = audio.createOscillator(); const gain = audio.createGain(); const now = audio.currentTime; osc.frequency.value = freq; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.16 * Number(volume.value) / 100, now + 0.01); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18); osc.connect(gain); gain.connect(audio.destination); osc.start(now); osc.stop(now + 0.2); } function setPhase(i) { index = i % phases.length; const p = phases[index]; startedAt = performance.now(); phase.textContent = p.title; hint.textContent = p.hint; steps.forEach(s => s.classList.toggle(“active”, s.dataset.step === p.key)); speak(p.word); beep(p.tone); } function loop(now) { if (!running) return; const total = Number(seconds.value); const elapsed = (now – startedAt) / 1000; const left = Math.max(1, Math.ceil(total – elapsed)); const progress = Math.min(1, elapsed / total); timer.textContent = left; if (phases[index].key === “inhale”) orb.style.transform = `scale(${0.72 + progress * 0.28})`; if (phases[index].key === “exhale”) orb.style.transform = `scale(${1 – progress * 0.28})`; if (elapsed >= total) { const next = (index + 1) % phases.length; if (next === 0) cycles++; cycle.textContent = `Cycle ${cycles}`; setPhase(next); } raf = requestAnimationFrame(loop); } function updateLabels() { secondsText.textContent = `${seconds.value} sec`; volumeText.textContent = `${volume.value}%`; timer.textContent = seconds.value; steps.forEach(s => s.querySelector(“small”).textContent = `${seconds.value} sec`); } start.onclick = async () => { if (running) { running = false; cancelAnimationFrame(raf); start.textContent = “Start”; status.textContent = “Paused.”; return; } if (audio && audio.state === “suspended”) await audio.resume(); chosenVoice = pickVoice(); running = true; start.textContent = “Stop”; status.textContent = “Running with spoken cues.”; setPhase(0); raf = requestAnimationFrame(loop); }; reset.onclick = () => { running = false; cancelAnimationFrame(raf); cycles = 0; index = 0; cycle.textContent = “Cycle 0”; phase.textContent = “Ready”; hint.textContent = “Inhale • pause • exhale • pause”; orb.style.transform = “scale(.72)”; steps.forEach(s => s.classList.remove(“active”)); start.textContent = “Start”; status.textContent = “Press Start to begin.”; updateLabels(); }; seconds.oninput = updateLabels; volume.oninput = updateLabels; if (“speechSynthesis” in window) speechSynthesis.onvoiceschanged = () => chosenVoice = pickVoice(); updateLabels(); })();