6 second breathing metronome
Cycle 0
Ready
Inhale • pause • exhale • pause
Inhale6 sec
Pause6 sec
Exhale6 sec
Pause6 sec
Seconds per phase
6 sec
Volume
65%
Meditative female voice
Start
Reset
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();
})();