DEV Community

Cover image for Calm in Code: Building an Interactive Breathing Exercise Web App
Learn Computer Academy
Learn Computer Academy

Posted on

Calm in Code: Building an Interactive Breathing Exercise Web App

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:

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Breathing Exercise Guide</title>
    <link rel="stylesheet" href="styles.css">
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;700&family=Lora:wght@400;700&display=swap" rel="stylesheet">
</head>
<body>
    <div class="container">
        <div class="controls">
            <h1>Breathing Guide</h1>
            <div class="settings">
                <select id="pattern">
                    <option value="478">4-7-8 Breathing</option>
                    <option value="box">Box Breathing</option>
                    <option value="diaphragm">Diaphragmatic</option>
                </select>
                <select id="level">
                    <option value="beginner">Beginner</option>
                    <option value="intermediate">Intermediate</option>
                    <option value="advanced">Advanced</option>
                </select>
                <button id="startBtn">Start</button>
                <button id="darkMode">Toggle Dark Mode</button>
            </div>
        </div>

        <div class="breathing-container">
            <div class="circle"></div>
            <div class="instructions">
                <span id="breathText">Breathe In</span>
                <span id="timer">4</span>
            </div>
        </div>

        <div class="progress">
            <div class="progress-bar" id="progressBar"></div>
            <span id="sessionTime">Session: 0:00</span>
        </div>
    </div>
    <script src="script.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The HTML structure is intentionally simple, with:

  1. A control section for selecting patterns and difficulty levels
  2. A central breathing circle that provides visual guidance
  3. Text instructions and a countdown timer
  4. 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;
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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 }
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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');
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    });
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Audio guidance: Adding optional sound cues for deeper immersion
  2. Custom breathing patterns: Allowing users to create and save personalized patterns
  3. Progressive web app: Making the app installable for offline use
  4. Breathing statistics: Tracking usage patterns over time
  5. Haptic feedback: Adding vibration for mobile users to follow without watching the screen

What I Learned

Building this app reinforced several important development concepts:

  1. Promises and async/await are powerful tools for handling sequential operations in a clean, readable way
  2. CSS animations can create meaningful UI beyond just aesthetic appeal
  3. Class-based organization keeps even simple applications maintainable
  4. 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!

ACI image

ACI.dev: The Only MCP Server Your AI Agents Need

ACI.dev’s open-source tool-use platform and Unified MCP Server turns 600+ functions into two simple MCP tools on one server—search and execute. Comes with multi-tenant auth and natural-language permission scopes. 100% open-source under Apache 2.0.

Star our GitHub!

Top comments (0)