Stay Hydrated: Building a Browser-Based Water Intake Tracker with Notifications

We all know staying hydrated is essential for our health, but tracking water consumption throughout the day can be challenging. In this tutorial, I'll walk you through creating a beautiful and functional web application that helps users monitor their water intake and sends timely reminders to drink more water. 💧

This project is perfect for intermediate web developers looking to strengthen their skills with practical application. We'll use vanilla JavaScript, CSS animations, and browser APIs to create a useful tool that works across modern browsers without any external libraries.

Project Overview

Our Water Intake Tracker features:

  • A visually appealing interface with an animated water glass
  • A circular progress indicator showing progress toward daily goals
  • Quick-add buttons for common water amounts
  • Custom input for precise tracking
  • Browser notification reminders
  • A historical log of daily water consumption
  • Persistent data storage using localStorage

You can see the final result here: Water Intake Tracker Demo

Understanding the Core Technologies

Before we dive into coding, let's understand the key technologies we'll be using:

Browser Notifications API

The Notifications API allows web applications to display system notifications outside the browser window. This is perfect for reminding users to drink water, even when they're working in other applications. The API requires user permission before sending notifications, which we'll handle in our code.

Local Storage

The localStorage API provides a simple way to store key-value pairs in the browser. Unlike cookies, localStorage data has no expiration time and persists even after the browser is closed. We'll use it to save the user's water intake data between sessions.

CSS Animations

Animations provide visual feedback and make our application more engaging. We'll use CSS keyframes to create a water filling animation and a ripple effect that simulates water movement.

SVG for Data Visualization

Scalable Vector Graphics (SVG) allow us to create the circular progress indicator that shows how close the user is to reaching their daily water goal. By manipulating the SVG's stroke properties, we can create a smooth, animated progress circle.

Object-Oriented JavaScript

We'll structure our code using JavaScript classes to organize our application logic. This approach makes our code more maintainable and easier to understand.

Project Structure

Let's examine the structure of our application:

  1. HTML: Creates the structure and layout of our tracker
  2. CSS: Styles the application and implements animations
  3. JavaScript: Provides the core functionality and interactivity

HTML Structure

The HTML provides the skeleton of our application. Let's break down the key sections:

</span>
 lang="en">

     charset="UTF-8">
     name="viewport" content="width=device-width, initial-scale=1.0">
    Water Intake Tracker
     rel="stylesheet" href="styles.css">
     href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">


     class="container">
        
            Water Intake Tracker
             class="subtitle">Stay hydrated, feel great
        

         class="main-section">
             class="progress-section">
                 class="glass-container">
                     class="glass">
                         class="water" id="waterLevel">
                    
                
                 class="progress-circle">
                    
                         class="bg-circle" cx="100" cy="100" r="90">
                         class="progress-ring" cx="100" cy="100" r="90">
                    
                     class="progress-text">
                         id="currentAmount">0
                         class="unit">ml
                    
                
                 class="goal">Goal:  id="dailyGoal">2000ml
            

             class="controls">
                 class="quick-add">
                     class="water-btn" data-amount="250">250ml
                     class="water-btn" data-amount="500">500ml
                     class="water-btn" data-amount="750">750ml
                

                 class="custom-add">
                     type="number" id="customAmount" placeholder="Custom amount (ml)">
                     id="addCustom">Add
                

                 class="reminder-toggle">
                     for="reminderSwitch">Reminders
                     type="checkbox" id="reminderSwitch">
                     id="reminderInterval">
                         value="30">Every 30 mins
                         value="60">Every 1 hour
                         value="120">Every 2 hours
                    
                
            
        

         class="history">
            Today's Log
             id="waterLog">
             id="resetDay">Reset Day
        
    

    <span class="na">src="script.js">





    Enter fullscreen mode
    


    Exit fullscreen mode
    




The HTML structure includes:
A container that holds the entire application
A header with the app title and subtitle
A main section containing the water glass visualization and progress circle
Controls for adding water (quick-add buttons and custom input)
A reminder toggle with interval selection
A history section showing the day's water intake log
The structure is designed to be semantic and accessible, with clear divisions between different functional areas of the application.
  
  
  CSS Styling
The CSS not only styles our application but also creates the animations that bring it to life. Let's look at some key styling features:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Poppins', sans-serif;
    background: linear-gradient(135deg, #e0f7fa, #b2ebf2);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
}

.container {
    background: rgba(255, 255, 255, 0.95);
    padding: 2rem;
    border-radius: 20px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
    width: 100%;
    max-width: 1200px;
    display: flex;
    flex-wrap: wrap;
    gap: 2rem;
}

header {
    text-align: center;
    width: 100%;
    margin-bottom: 1rem;
}

h1 {
    color: #0277bd;
    font-weight: 600;
}

.subtitle {
    color: #666;
    font-size: 0.9rem;
}

.main-section {
    flex: 2;
    min-width: 300px;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.progress-section {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    gap: 2rem;
    flex-wrap: wrap;
}

.glass-container {
    width: 160px;
    height: 250px;
    position: relative;
}

.glass {
    width: 100%;
    height: 100%;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 0 0 20px 20px;
    border: 3px solid #0288d1;
    border-top: none;
    overflow: hidden;
}

.water {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    background: linear-gradient(135deg, #81d4fa, #0288d1);
    transition: height 1s ease-in;
    border-radius: 0 0 17px 17px;
    overflow: hidden;
}

.water.filling {
    animation: waveFlow 1.5s ease-in forwards;
}

@keyframes waveFlow {
    0% { height: var(--start-height, 0px); opacity: 0.8; }
    50% { opacity: 1; background: linear-gradient(135deg, #b3e5fc, #0288d1); }
    100% { height: var(--final-height); opacity: 1; background: linear-gradient(135deg, #81d4fa, #0288d1); }
}

.water::before, .water::after {
    content: '';
    position: absolute;
    top: 0;
    width: 200%;
    height: 20px;
    background: rgba(255, 255, 255, 0.4);
    animation: waveRipple 2s infinite ease-in-out;
}

.water::after {
    left: -75%;
    height: 15px;
    background: rgba(255, 255, 255, 0.3);
    animation: waveRipple 2.5s infinite ease-in-out reverse;
}

@keyframes waveRipple {
    0% { transform: translateX(0); }
    50% { transform: translateX(50%); }
    100% { transform: translateX(0); }
}

.progress-circle {
    position: relative;
    width: 200px;
    height: 200px;
}

svg {
    width: 100%;
    height: 100%;
}

.bg-circle {
    fill: none;
    stroke: #e0e0e0;
    stroke-width: 20;
}

.progress-ring {
    fill: none;
    stroke: #0288d1;
    stroke-width: 20;
    stroke-linecap: round;
    transform: rotate(-90deg);
    transform-origin: 50% 50%;
    transition: stroke-dashoffset 0.5s ease;
}

.progress-text {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
}

#currentAmount {
    font-size: 2.5rem;
    font-weight: 600;
    color: #0277bd;
    display: block;
}

.unit {
    font-size: 1rem;
    color: #666;
}

.goal {
    color: #444;
    text-align: center;
    width: 100%;
}

.controls {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
}

.quick-add {
    display: flex;
    gap: 1rem;
    justify-content: center;
}

.water-btn {
    padding: 0.8rem 1.5rem;
    border: none;
    border-radius: 25px;
    background: #0288d1;
    color: white;
    cursor: pointer;
    transition: transform 0.2s, background 0.2s;
}

.water-btn:hover {
    transform: scale(1.05);
    background: #0277bd;
}

.custom-add {
    display: flex;
    gap: 0.5rem;
}

#customAmount {
    padding: 0.8rem;
    border: 2px solid #e0e0e0;
    border-radius: 10px;
    flex-grow: 1;
}

#addCustom {
    padding: 0.8rem 1.5rem;
    border: none;
    border-radius: 10px;
    background: #4caf50;
    color: white;
    cursor: pointer;
}

#addCustom:hover {
    background: #45a049;
}

.reminder-toggle {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.reminder-toggle label {
    color: #444;
}

#reminderSwitch {
    width: 40px;
    height: 20px;
}

#reminderInterval {
    padding: 0.5rem;
    border-radius: 5px;
    border: 1px solid #e0e0e0;
}

.history {
    flex: 1;
    min-width: 300px;
    background: #f5f5f5;
    padding: 1rem;
    border-radius: 15px;
}

h3 {
    color: #0277bd;
    margin-bottom: 1rem;
}

#waterLog {
    list-style: none;
    max-height: 150px;
    overflow-y: auto;
    margin-bottom: 1rem;
}

#waterLog li {
    padding: 0.5rem;
    border-bottom: 1px solid #e0e0e0;
    color: #666;
}

#resetDay {
    width: 100%;
    padding: 0.8rem;
    border: none;
    border-radius: 10px;
    background: #ef5350;
    color: white;
    cursor: pointer;
}

#resetDay:hover {
    background: #e53935;
}

/* Responsive adjustments */
@media (max-width: 900px) {
    .container {
        flex-direction: column;
        max-width: 600px;
    }
    .progress-section {
        flex-direction: column;
        gap: 1rem;
    }
    .goal {
        margin-top: 1rem;
    }
}



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  Water Glass Animation
The water glass animation is created using several techniques:
A glass container with a semi-transparent border
A water element that changes height based on the amount of water consumed
Pseudo-elements (::before and ::after) that create the ripple effect on the water surface
CSS keyframes that animate the filling effect and ripple movement
When water is added, we dynamically set CSS custom properties (--start-height and --final-height) and add a 'filling' class to trigger the animation.
  
  
  Progress Circle
The circular progress indicator uses SVG. It consists of:
A background circle that represents the total goal
A progress circle that fills in as water is consumed
The progress is visualized by manipulating the stroke-dasharray and stroke-dashoffset properties. As the user adds water, the dashoffset decreases, revealing more of the circle.
  
  
  Responsive Design
Our application is fully responsive, adapting to different screen sizes:
On larger screens, the layout is horizontal with the water glass and progress circle side by side
On smaller screens, the layout shifts to a vertical arrangement
Flexible sizing ensures the app looks good on all devices

  
  
  JavaScript Functionality
The JavaScript code is the heart of our application, providing all the interactive functionality. Let's examine the key components:

class WaterTracker {
    constructor() {
        this.dailyGoal = 2000;
        this.currentAmount = 0;
        this.log = [];
        this.reminderInterval = null;

        this.elements = {
            currentAmount: document.getElementById('currentAmount'),
            progressRing: document.querySelector('.progress-ring'),
            waterLog: document.getElementById('waterLog'),
            customAmount: document.getElementById('customAmount'),
            reminderSwitch: document.getElementById('reminderSwitch'),
            reminderIntervalSelect: document.getElementById('reminderInterval'),
            waterLevel: document.getElementById('waterLevel')
        };

        this.init();
    }

    init() {
        this.requestNotificationPermission();
        this.loadFromLocalStorage();
        this.setupEventListeners();
        this.updateProgress();
    }

    requestNotificationPermission() {
        if (Notification.permission !== 'granted') {
            Notification.requestPermission();
        }
    }

    setupEventListeners() {
        document.querySelectorAll('.water-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const amount = parseInt(btn.dataset.amount);
                this.addWater(amount);
            });
        });

        document.getElementById('addCustom').addEventListener('click', () => {
            const amount = parseInt(this.elements.customAmount.value);
            if (amount > 0) {
                this.addWater(amount);
                this.elements.customAmount.value = '';
            }
        });

        document.getElementById('resetDay').addEventListener('click', () => {
            this.resetDay();
        });

        this.elements.reminderSwitch.addEventListener('change', () => {
            this.toggleReminders();
        });

        this.elements.reminderIntervalSelect.addEventListener('change', () => {
            if (this.elements.reminderSwitch.checked) {
                this.toggleReminders();
                this.toggleReminders();
            }
        });
    }

    addWater(amount) {
        const previousAmount = this.currentAmount;
        this.currentAmount += amount;
        const timestamp = new Date().toLocaleTimeString();
        this.log.unshift(`${amount}ml at ${timestamp}`);

        const glassHeight = 250;
        const startHeight = (previousAmount / this.dailyGoal) * glassHeight;
        const finalHeight = (this.currentAmount / this.dailyGoal) * glassHeight;

        this.elements.waterLevel.style.setProperty('--start-height', `${Math.min(startHeight, glassHeight)}px`);
        this.elements.waterLevel.style.setProperty('--final-height', `${Math.min(finalHeight, glassHeight)}px`);

        this.elements.waterLevel.classList.remove('filling');
        void this.elements.waterLevel.offsetWidth;
        this.elements.waterLevel.classList.add('filling');

        this.updateProgress();
        this.updateLog();
        this.saveToLocalStorage();
    }

    updateProgress() {
        this.elements.currentAmount.textContent = this.currentAmount;

        const circumference = 2 * Math.PI * 90;
        const progress = Math.min(this.currentAmount / this.dailyGoal, 1);
        const offset = circumference * (1 - progress);
        this.elements.progressRing.style.strokeDasharray = circumference;
        this.elements.progressRing.style.strokeDashoffset = offset;

        const glassHeight = 250;
        const waterHeight = (this.currentAmount / this.dailyGoal) * glassHeight;
        this.elements.waterLevel.style.height = `${Math.min(waterHeight, glassHeight)}px`;
    }

    updateLog() {
        this.elements.waterLog.innerHTML = this.log
            .map(entry => `${entry}`)
            .join('');
    }

    resetDay() {
        this.currentAmount = 0;
        this.log = [];
        this.updateProgress();
        this.updateLog();
        this.saveToLocalStorage();
        this.elements.waterLevel.style.height = '0px';
    }

    toggleReminders() {
        if (this.elements.reminderSwitch.checked) {
            if (this.reminderInterval) clearInterval(this.reminderInterval);
            const interval = parseInt(this.elements.reminderIntervalSelect.value) * 60 * 1000;
            this.reminderInterval = setInterval(() => {
                if (Notification.permission === 'granted') {
                    new Notification('Time to Hydrate!', {
                        body: 'Drink some water to stay healthy!',
                        icon: 'https://cdn-icons-png.flaticon.com/512/824/824748.png'
                    });
                }
            }, interval);
        } else {
            if (this.reminderInterval) {
                clearInterval(this.reminderInterval);
                this.reminderInterval = null;
            }
        }
    }

    saveToLocalStorage() {
        localStorage.setItem('waterTracker', JSON.stringify({
            currentAmount: this.currentAmount,
            log: this.log,
            date: new Date().toDateString()
        }));
    }

    loadFromLocalStorage() {
        const data = JSON.parse(localStorage.getItem('waterTracker'));
        if (data && data.date === new Date().toDateString()) {
            this.currentAmount = data.currentAmount;
            this.log = data.log;
            this.updateProgress();
            this.updateLog();
        }
    }
}

document.addEventListener('DOMContentLoaded', () => {
    new WaterTracker();
});



    Enter fullscreen mode
    


    Exit fullscreen mode
    





  
  
  The WaterTracker Class
We use a class-based approach to organize our code. The WaterTracker class:
Initializes the application state
Sets up event listeners
Manages water intake data
Handles browser notifications
Interacts with localStorage

  
  
  Water Tracking Logic
The core functionality revolves around tracking water consumption:
When the user adds water (via quick-add buttons or custom input), the current amount is updated
The water glass animation is triggered
The progress circle is updated
A new entry is added to the log
The data is saved to localStorage

  
  
  Animation Logic
The animation logic is particularly interesting:
We calculate the start and end heights based on the previous and current water amounts
We set these as CSS custom properties
We briefly remove and reapply the 'filling' class to reset the animation
The CSS animation then transitions the water level smoothly

  
  
  Browser Notifications
The notification system works as follows:
We request permission to send notifications when the app initializes
When the user enables reminders, we set up an interval based on their selected frequency
At each interval, we send a notification if permission has been granted
When reminders are disabled, we clear the interval

  
  
  Data Persistence
To ensure data persists between sessions:
We save the current amount, log, and date to localStorage whenever water is added
When the app initializes, we check if there's saved data from the current day
If there is, we load it; if not, we start fresh

  
  
  How It All Works Together
When the page loads, the following sequence occurs:
The HTML is rendered, creating the structure of the application
The CSS styles are applied, setting up the initial appearance
When the DOM is fully loaded, our JavaScript code instantiates the WaterTracker class
The WaterTracker initializes the application state, sets up event listeners, and loads any saved data
The user can now interact with the application
When the user adds water:
The addWater method updates the current amount and log
The updateProgress method updates the visual indicators
The water glass animation is triggered
The data is saved to localStorage
When the user toggles reminders:
The toggleReminders method either sets up or clears the notification interval
If enabled, the user will receive notifications at their selected interval

  
  
  Understanding the Code in Depth
Let's dive deeper into some of the more complex parts of the code:
  
  
  The Water Glass Animation
The water glass animation combines several techniques:
CSS custom properties to set the initial and final heights
The offsetWidth property to force a reflow between removing and adding the 'filling' class
Keyframe animations to create the filling effect and ripple movement
This creates a smooth, visually appealing animation that provides immediate feedback when water is added.
  
  
  The Progress Circle Calculation
The progress circle uses SVG stroke properties to show progress:
We calculate the circumference of the circle
We determine the progress as a percentage of the daily goal
We set the stroke-dasharray to the circumference
We set the stroke-dashoffset to the circumference multiplied by (1 - progress)
As the user adds water, the offset decreases, revealing more of the circle.
  
  
  Handling Date Changes
Our application needs to reset at the start of each new day:
When saving data, we include the current date
When loading data, we check if the saved date matches the current date
If not, we start fresh with zero water intake
This ensures that users start each day with a clean slate.
  
  
  Potential Enhancements
While our Water Intake Tracker is fully functional, there are several ways it could be enhanced:

User Settings: Allow users to customize their daily water goal based on their weight, activity level, or climate

Data Visualization: Add graphs to show water intake patterns over time

Multiple Beverage Types: Track different types of liquids with different hydration values

Achievements: Implement badges or achievements to gamify the hydration process

Sync Across Devices: Add a backend to sync data across multiple devices

  
  
  Conclusion
Building a Water Intake Tracker is not only a practical exercise in web development but also creates a useful tool that can help users maintain healthy hydration habits. Through this project, we've explored:
Creating interactive web applications with vanilla JavaScript
Implementing CSS animations and SVG manipulation
Working with browser APIs like Notifications and localStorage
Using object-oriented programming principles to organize code
Building responsive designs that work across devices
The skills learned in this project can be applied to many other web applications, making it an excellent addition to any developer's portfolio. 💧Remember, the key to good health is staying hydrated, and the key to good code is staying organized!
  
  
  Resources for Further Learning
If you're interested in learning more about the technologies used in this project, here are some excellent resources:
MDN Web Docs: Notifications API
MDN Web Docs: localStorage
MDN Web Docs: CSS Animations
MDN Web Docs: SVG
JavaScript.info: Object-oriented JavaScript