Ever feel like your coding sessions leave you tense and stressed? I've been there too! That's why I created a breathing exercise web application that helps developers (and everyone else) practice mindful breathing techniques with visual guidance.
In this post, I'll walk through how I built this app using vanilla JavaScript, CSS animations, and a clean, responsive design. Whether you're looking to build something similar or just want to add some interactive animations to your projects, there's something here for you!
Demo
You can try the live app here: Breathing Exercise Guide
The Why Behind the App
As developers, we often forget to breathe properly while focusing intensely on debugging or building new features. Research shows that controlled breathing can:
- Reduce mental fatigue and stress
- Improve problem-solving abilities
- Increase focus and concentration
- Lower physical tension from long coding sessions
Each breathing pattern in the app serves a specific purpose:
- 4-7-8 Breathing: Great for rapid relaxation and breaking stress cycles
- Box Breathing: Enhances focus and clarity (used by Navy SEALs!)
- Diaphragmatic Breathing: Improves oxygen flow and reduces shallow breathing habits
App Features
Here's what our breathing app includes:
- Multiple breathing patterns with different timing sequences
- Three difficulty levels to accommodate users of all experience levels
- Visual animations that expand and contract with breathing rhythm
- A countdown timer for each breathing phase
- Session tracking to monitor practice time
- Dark mode toggle for reduced eye strain (especially important for night coding!)
The HTML Structure
Let's start with the HTML backbone of our application:
charset="UTF-8">
name="viewport" content="width=device-width, initial-scale=1.0">
Breathing Exercise Guide
rel="stylesheet" href="styles.css">
href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;700&family=Lora:wght@400;700&display=swap" rel="stylesheet">
class="container">
class="controls">
Breathing Guide
class="settings">
id="pattern">
value="478">4-7-8 Breathing
value="box">Box Breathing
value="diaphragm">Diaphragmatic
id="level">
value="beginner">Beginner
value="intermediate">Intermediate
value="advanced">Advanced
id="startBtn">Start
id="darkMode">Toggle Dark Mode
class="breathing-container">
class="circle">
class="instructions">
id="breathText">Breathe In
id="timer">4
class="progress">
class="progress-bar" id="progressBar">
id="sessionTime">Session: 0:00
<span class="na">src="script.js">
The HTML structure is intentionally simple, with:
- A control section for selecting patterns and difficulty levels
- A central breathing circle that provides visual guidance
- Text instructions and a countdown timer
- A progress tracking section
CSS: Bringing the Breath to Life
The visual magic happens in the CSS:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body {
background: linear-gradient(135deg, #e0f2fe, #bae6fd);
min-height: 100vh;
transition: background 0.3s;
}
body.dark {
background: linear-gradient(135deg, #1e3a8a, #3b82f6);
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.controls {
text-align: center;
margin-bottom: 40px;
color: #1e3a8a;
}
body.dark .controls {
color: #f1f5f9;
}
h1 {
font-family: 'Poppins', sans-serif;
font-weight: 700;
font-size: 2.8rem;
margin-bottom: 20px;
letter-spacing: 1px;
}
.settings {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
select, button {
padding: 10px 20px;
border: none;
border-radius: 25px;
background: #ffffff;
cursor: pointer;
font-family: 'Lora', serif;
font-size: 1rem;
font-weight: 400;
transition: all 0.3s ease;
}
body.dark select,
body.dark button {
background: #1e40af;
color: #ffffff;
}
select:hover, button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.breathing-container {
position: relative;
height: 400px;
display: flex;
justify-content: center;
align-items: center;
}
.circle {
width: 200px;
height: 200px;
background: radial-gradient(circle, #3b82f6 0%, #93c5fd 70%, transparent 100%);
border-radius: 50%;
position: absolute;
transition: all 1s ease-in-out;
}
body.dark .circle {
background: radial-gradient(circle, #60a5fa 0%, #1e40af 70%, transparent 100%);
}
.inhale {
transform: scale(1.5);
}
.hold {
transform: scale(1.2);
}
.exhale {
transform: scale(1);
}
.instructions {
position: relative;
text-align: center;
color: #1e3a8a;
z-index: 1;
}
body.dark .instructions {
color: #f1f5f9;
}
#breathText {
display: block;
font-family: 'Poppins', sans-serif;
font-weight: 300;
font-size: 2.2rem;
margin-bottom: 10px;
letter-spacing: 0.5px;
}
#timer {
font-family: 'Lora', serif;
font-weight: 700;
font-size: 3.5rem;
line-height: 1;
}
.progress {
margin-top: 40px;
text-align: center;
}
.progress-bar {
width: 100%;
height: 12px;
background: #dbeafe;
border-radius: 6px;
overflow: hidden;
margin-bottom: 10px;
position: relative;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
}
body.dark .progress-bar {
background: #1e40af;
}
.progress-bar::after {
content: '';
display: block;
height: 100%;
width: 0;
background: linear-gradient(90deg, #3b82f6, #10b981);
border-radius: 6px;
transition: width 0.5s ease-out;
position: absolute;
left: 0;
top: 0;
}
body.dark .progress-bar::after {
background: linear-gradient(90deg, #60a5fa, #34d399);
}
#sessionTime {
font-family: 'Lora', serif;
font-weight: 400;
color: #1e3a8a;
font-size: 1.2rem;
letter-spacing: 0.5px;
}
body.dark #sessionTime {
color: #f1f5f9;
}
The most crucial part of our styling involves the breathing animations. We use CSS transitions to smoothly scale the circle, creating a visual representation of the breathing cycle:
.circle {
/* Base styles for the circle */
transition: all 1s ease-in-out;
}
.inhale {
transform: scale(1.5);
}
.hold {
transform: scale(1.2);
}
.exhale {
transform: scale(1);
}
By applying these classes dynamically with JavaScript, we create the illusion of the circle "breathing" along with the user.
JavaScript: The Mindful Engine
The functionality is powered by a class-based JavaScript approach:
class BreathingGuide {
constructor() {
this.circle = document.querySelector('.circle');
this.breathText = document.getElementById('breathText');
this.timerDisplay = document.getElementById('timer');
this.startBtn = document.getElementById('startBtn');
this.patternSelect = document.getElementById('pattern');
this.levelSelect = document.getElementById('level');
this.progressBar = document.getElementById('progressBar');
this.sessionTimeDisplay = document.getElementById('sessionTime');
this.darkModeBtn = document.getElementById('darkMode');
this.isRunning = false;
this.sessionTime = 0;
this.patterns = {
'478': { inhale: 4, hold: 7, exhale: 8 },
'box': { inhale: 4, hold: 4, exhale: 4, holdOut: 4 },
'diaphragm': { inhale: 6, hold: 2, exhale: 6 }
};
this.levels = {
'beginner': 0.8,
'intermediate': 1,
'advanced': 1.2
};
this.initEvents();
}
initEvents() {
this.startBtn.addEventListener('click', () => this.toggleExercise());
this.darkModeBtn.addEventListener('click', () =>
document.body.classList.toggle('dark'));
}
toggleExercise() {
if (this.isRunning) {
this.stop();
} else {
this.start();
}
}
start() {
this.isRunning = true;
this.startBtn.textContent = 'Stop';
this.sessionTime = 0;
this.runSession();
this.sessionInterval = setInterval(() => this.updateSessionTime(), 1000);
}
stop() {
this.isRunning = false;
this.startBtn.textContent = 'Start';
clearInterval(this.currentInterval);
clearInterval(this.sessionInterval);
this.circle.className = 'circle';
this.breathText.textContent = 'Paused';
this.timerDisplay.textContent = '0';
}
async runSession() {
const pattern = this.patterns[this.patternSelect.value];
const speed = this.levels[this.levelSelect.value];
while (this.isRunning) {
await this.breathe('Inhale', pattern.inhale / speed, 'inhale');
if (!this.isRunning) break;
await this.breathe('Hold', pattern.hold / speed, 'hold');
if (!this.isRunning) break;
await this.breathe('Exhale', pattern.exhale / speed, 'exhale');
if (pattern.holdOut && this.isRunning) {
await this.breathe('Hold', pattern.holdOut / speed, 'hold');
}
}
}
breathe(text, duration, animation) {
return new Promise(resolve => {
this.breathText.textContent = text;
this.circle.className = `circle ${animation}`;
let timeLeft = Math.round(duration);
this.timerDisplay.textContent = timeLeft;
this.currentInterval = setInterval(() => {
timeLeft--;
this.timerDisplay.textContent = timeLeft;
if (timeLeft <= 0) {
clearInterval(this.currentInterval);
resolve();
}
}, 1000);
const progressPercentage = Math.min((this.sessionTime / 300) * 100, 100);
this.progressBar.style.width = `${progressPercentage}%`;
});
}
updateSessionTime() {
this.sessionTime++;
const minutes = Math.floor(this.sessionTime / 60);
const seconds = this.sessionTime % 60;
this.sessionTimeDisplay.textContent =
`Session: ${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
}
}
document.addEventListener('DOMContentLoaded', () => {
new BreathingGuide();
});
Let's break down the key components:
The Core Breathing Patterns
Each breathing pattern has distinct timing configurations:
this.patterns = {
'478': { inhale: 4, hold: 7, exhale: 8 },
'box': { inhale: 4, hold: 4, exhale: 4, holdOut: 4 },
'diaphragm': { inhale: 6, hold: 2, exhale: 6 }
};
Difficulty Levels with Speed Multipliers
To make the app accessible to everyone, I implemented adjustable speeds:
this.levels = {
'beginner': 0.8, // Slower for beginners
'intermediate': 1,
'advanced': 1.2 // Faster for experienced users
};
Async/Await for Smooth Sequences
The most elegant solution for creating sequential breathing phases came through using async/await:
async runSession() {
const pattern = this.patterns[this.patternSelect.value];
const speed = this.levels[this.levelSelect.value];
while (this.isRunning) {
// Execute each phase in sequence
await this.breathe('Inhale', pattern.inhale / speed, 'inhale');
if (!this.isRunning) break;
await this.breathe('Hold', pattern.hold / speed, 'hold');
if (!this.isRunning) break;
await this.breathe('Exhale', pattern.exhale / speed, 'exhale');
if (pattern.holdOut && this.isRunning) {
await this.breathe('Hold', pattern.holdOut / speed, 'hold');
}
}
}
This creates a continuous, uninterrupted flow through each breathing phase, which is essential for a guided breathing experience.
The Promise-Based Breathing Method
Each breathing phase is handled by a Promise that resolves when the timer completes:
breathe(text, duration, animation) {
return new Promise(resolve => {
// Update UI
this.breathText.textContent = text;
this.circle.className = `circle ${animation}`;
// Set up countdown
let timeLeft = Math.round(duration);
this.timerDisplay.textContent = timeLeft;
// Start the timer
this.currentInterval = setInterval(() => {
timeLeft--;
this.timerDisplay.textContent = timeLeft;
if (timeLeft <= 0) {
clearInterval(this.currentInterval);
resolve(); // Move to next phase
}
}, 1000);
});
}
Technical Challenges I Faced
Challenge 1: Creating a Seamless Flow
Initially, I tried using nested setTimeout
functions to move through breathing phases, but this quickly became unwieldy and difficult to maintain.
Solution: Converting to a Promise-based approach with async/await provided a clean, readable way to handle sequential steps while making the code much more maintainable.
Challenge 2: Preventing Timing Drift
With any timer-based application, timing drift can be an issue—where small inaccuracies add up over time.
Solution: While setInterval
isn't perfect, using it for second-by-second countdown (rather than for the entire phase duration) minimizes noticeable drift while maintaining a simple implementation.
Challenge 3: Handling Interruptions
Users may want to stop mid-session, which required careful handling of ongoing timers and animations.
Solution: Implementing comprehensive cleanup in the stop()
method ensures all intervals are cleared and animations reset properly when the user interrupts a session.
Potential Improvements
There's always room for enhancement! Here are some ideas I'm considering:
- Audio guidance: Adding optional sound cues for deeper immersion
- Custom breathing patterns: Allowing users to create and save personalized patterns
- Progressive web app: Making the app installable for offline use
- Breathing statistics: Tracking usage patterns over time
- Haptic feedback: Adding vibration for mobile users to follow without watching the screen
What I Learned
Building this app reinforced several important development concepts:
- Promises and async/await are powerful tools for handling sequential operations in a clean, readable way
- CSS animations can create meaningful UI beyond just aesthetic appeal
- Class-based organization keeps even simple applications maintainable
- User experience considerations go beyond just functionality—finding the right pacing for different user levels required thoughtful testing
Wrap Up
This breathing exercise app started as a personal project to help manage my own coding stress, but it's become a tool I use daily. The web is full of complex applications, but sometimes the most useful ones are elegantly simple.
Have you built tools to improve your wellbeing or productivity? What breathing techniques do you find helpful during intense coding sessions? I'd love to hear your thoughts and suggestions in the comments!
P.S. If you're interested in the complete source code or have questions about implementation details, let me know in the comments!