This content originally appeared on DEV Community and was authored by Learn Computer Academy
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:
- HTML: Creates the structure and layout of our tracker
- CSS: Styles the application and implements animations
- JavaScript: Provides the core functionality and interactivity
HTML Structure
The HTML provides the skeleton of our application. Let’s break down the key sections:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Water Intake Tracker</title>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<header>
<h1>Water Intake Tracker</h1>
<p class="subtitle">Stay hydrated, feel great</p>
</header>
<div class="main-section">
<div class="progress-section">
<div class="glass-container">
<div class="glass">
<div class="water" id="waterLevel"></div>
</div>
</div>
<div class="progress-circle">
<svg>
<circle class="bg-circle" cx="100" cy="100" r="90"></circle>
<circle class="progress-ring" cx="100" cy="100" r="90"></circle>
</svg>
<div class="progress-text">
<span id="currentAmount">0</span>
<span class="unit">ml</span>
</div>
</div>
<div class="goal">Goal: <span id="dailyGoal">2000</span>ml</div>
</div>
<div class="controls">
<div class="quick-add">
<button class="water-btn" data-amount="250">250ml</button>
<button class="water-btn" data-amount="500">500ml</button>
<button class="water-btn" data-amount="750">750ml</button>
</div>
<div class="custom-add">
<input type="number" id="customAmount" placeholder="Custom amount (ml)">
<button id="addCustom">Add</button>
</div>
<div class="reminder-toggle">
<label for="reminderSwitch">Reminders</label>
<input type="checkbox" id="reminderSwitch">
<select id="reminderInterval">
<option value="30">Every 30 mins</option>
<option value="60">Every 1 hour</option>
<option value="120">Every 2 hours</option>
</select>
</div>
</div>
</div>
<div class="history">
<h3>Today's Log</h3>
<ul id="waterLog"></ul>
<button id="resetDay">Reset Day</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
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;
}
}
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 => `<li>${entry}</li>`)
.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();
});
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
This content originally appeared on DEV Community and was authored by Learn Computer Academy