I’m a huge fan of the pomodoro technique, and use both an analogue and digital one, depending on if I’m working at my main computer or my laptop. I just love them so much!
Thatβs Cool but What Is a Pomodoro Timer?
A Pomodoro timer is a time management instrument that divides your time into segments, typically lasting 25 minutes, known as ‘Pomodoros’. Following each Pomodoro, a brief pause of around 5 minutes is taken before beginning the next interval. After completing four Pomodoros, a more extended break ranging from 15 to 30 minutes is taken, after which the cycle recommences.
Last week I thought it would be a fun project to create, and learn from, so here we are. Just note that I don’t integrate the extended breaks into my sessions, so I have purposefully left this feature out.
What Features Will this Pomodoro Timer Have?
Just to note, this project will be only using HTML, CSS, and Vanilla JavaScript.
Here are the features that I have settled on:
- Settings toggle area.
- Text area to display if timer is paused or in rest mode.
- User controllable for both work and rest timers.
- SVG circular progress bar to indicate how much time is left.
- Number of completed work sessions.
- Play / pause button to control timer.
- Music to indicate when work / rest timers have finished
- Timer countdown is displayed in the browser tab.
- Background image dims when timer in rest mode.
Table of Contents
Right, enough spiel, let’s jump right in and build a pomodoro timer, baby!
You can view the demo of this pomodoro timer here.
1. Set Up Our Pomodoro Timer HTML, CSS & JavaScript
As always, let’s create the foundation first, then we can start building the house π
- index.htm
- style.css
- main.js
- Result
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pomodoro Timer | Frontend Hero</title>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="vista-bg"></div>
<div id="feh-pomodoro"></div>
<script src="main.js"></script>
</body>
</html>
/**
* Quick and dirty reset & wrapper set-up
*/
* { padding: 0; margin: 0; box-sizing: border-box; }
html, body { height: 100%; font-family: uniform; }
.wrapper {
position: relative;
width: 100%;
max-width: 1100px;
margin: 0 auto;
padding: 0 15px;
}
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
#vista-bg {
background-repeat: no-repeat;
background-size: cover;
background-position: center;
background-image: url(https://www.frontendhero.dev/wp-content/uploads/2023/05/countryside-day.png);
height: 100%;
width: 100%;
position: absolute;
transition: all 0.6s ease-in-out;
}
#feh-pomodoro {
position: relative;
width: 100%;
max-width: 350px;
margin: 0 auto;
padding: 55px 25px 25px 25px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.29);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(7.7px);
-webkit-backdrop-filter: blur(7.7px);
border: 1px solid rgba(255, 255, 255, 0.64);
}
// coming soon
We have created three files:
index.htm
– We have hooked up our CSS, JS, added a vista & pomodoro div.style.css
– Reset the page’s elements, added a wrapper, and centred the body with flexbox.main.js
– Nada yet!
And have added some minimal styles in our stylesheet to reset elements, used flexbox to centre our pomodoro timer, add the vista background, and we are using the Font Awesome library, as I do so much in these tutorials.
2. Writing The Pomodoro Markup For The User Interface
Below are the main sections of our pomodoro timer.
- index.htm
<div id="feh-pomodoro">
<div id="feh-pomodoro-overlay"></div>
<span class="btn-icon" id="feh-toggle-settings"><i class="fa fa-cog"></i></span>
<div id="feh-timer-progress"></div>
<div id="feh-timer-sessions"></div>
<div id="feh-timer-functions">
<div id="feh-timer-settings"></div>
<div id="feh-timer-buttons"></div>
</div>
</div>
… and an image to illustrate the different parts.
Let’s go ahead flesh out this puppy.
3. Style the Pomodoro Timer Overlay
I’ve chosen to add an overlay div inside my pomodoro timer because as the page loads, the elements inside fade in/shift around a bit and a nice little ‘fix’ for this; to make it look nicer is to simply hide everything with this overlay div and then fade it out, once the page fully loads π
- index.htm
- style.css
- main.js
<div id="feh-pomodoro-overlay">
<img src="spinner-red.gif" alt="">
</div>
#feh-pomodoro-overlay {
background: #fff;
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
border-radius: 20px;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease-in-out;
}
body.page-loaded #feh-pomodoro-overlay {
opacity: 0;
visibility: hidden;
}
#feh-pomodoro-overlay img {
width: 150px;
height: 150px;
}
(function () {
/**
* Declare vars
*/
const fehBody = document.body;
/**
*
*/
window.addEventListener("load", () => {
fehBody.classList.add('page-loaded');
});
})();
Notes
I went ahead and created a loading spinner image and added it inside my #feh-pomodoro-overlay
div, then just tweaked the size.
Then I made my overlay a white background – which will hide all of the upcoming elements that might look a little messy upon page load.
One important part is this part:
body.page-loaded #feh-pomodoro-overlay {
opacity: 0;
visibility: hidden;
}
In the main.js file, I’ve waited until the page fully loads, and then added this class to the body element, that will hide the overlay and reveal our timer.
4. Positioning the Settings Button
The settings button looks a little out of place, so let’s get that into position.
I’ve created a generic .btn-icon
class also, as there will be another button using these values a bit later on.
- style.css
- Result
.btn-icon {
border-radius: 20px;
position: absolute;
right: 0;
width: 50px;
display: flex;
font-size: 24px;
justify-content: center;
color: #7b7b7b;
padding-top: 10px;
cursor: pointer;
transition: all 0.3s ease-in-out;
}
#feh-toggle-settings {
top: 20px;
right: 20px;
height: 50px;
}
5. Pomodoro Timer Progress Area
Now let’s build the circular progress area β this is where the progress of the timer will animate until it’s completed.
I originally used the canvas API but I couldn’t get the progress circle to look crisp on my retina screen, so I abandoned that path, and ended up using an SVG instead.
All we need to do is create a basic SVG circle as shown below, coupled with some text elements (hidden for the moment) that will be used a little later on. This will be housed inside the #feh-timer-progress
element.
- index.htm
- style.css
- Result
<div id="feh-timer-progress">
<svg class="circle-timer" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" class="circle-background"/>
<circle cx="50" cy="50" r="45" class="circle-progress"/>
<text id="feh-timer-time" x="50" y="50" text-anchor="middle" dy=".3em" font-size="18">00:00</text>
<text id="feh-timer-pause" x="50" y="68" text-anchor="middle" dy=".3em" font-size="7">Paused</text>
<text id="feh-timer-rest" x="50" y="68" text-anchor="middle" dy=".3em" font-size="7">Rest</text>
</svg>
</div>
#feh-timer-progress {
display: flex;
justify-content: center;
margin-bottom: 40px;
}
.circle-timer {
width: 270px;
height: 270px;
}
.circle-background {
stroke: #ffffff;
stroke-width: 7;
fill: none;
}
.circle-progress {
stroke: #fd5252;
stroke-width: 7;
stroke-dasharray: 283;
stroke-linecap: round;
fill: none;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
#feh-timer-time {
fill: #5c5c5c;
}
#feh-timer-pause,
#feh-timer-rest {
opacity: 0;
transition: all 0.3s ease-in-out;
fill: #5c5c5cd1;
}
All should be fairly ok to understand β the main parts to look at are:
stroke-dasharray: 283;
transform: rotate(-90deg);
You’ll see a little later on why I added 283 as the value here, but basically this is the circumference.
Then I just rotated the progress element to bring it to 12 O’clock.
6. Building out the Completed Sessions Area
I like to keep track of how many session’s I’ve done, so I thought it would be a nice addition to add this feature in. Every ‘work’ session that is completed, the number will increment by 1.
- index.htm
- style.css
- Result
<div id="feh-timer-sessions">
<p id="feh-completed-label">Completed work sessions: </p>
<p id="feh-completed-sessions">0</p>
</div>
#feh-timer-sessions {
border-radius: 50px;
background: #fff;
display: flex;
color: #5c5c5cb8;
align-items: center;
transition: all 0.3s ease-in-out;
}
#feh-timer-sessions p {
padding: 12px;
}
p#feh-completed-label {
width: 80%;
text-align: center
}
p#feh-completed-sessions {
font-weight: bold;
color: #222;
font-size: 20px;
}
We’ll make this update later on when we’re writing our JavaScript.
7. Adding Our Play and Pause Buttons
Next we’ll add in arguably the most important part of this whole pomodoro timer, adding a way to start this pomodoro timer! (and pause it).
I just have to add in the CSS for the parent element named #feh-timer-functions
that will hold this area, and our settings. This will give this parent element a fixed height, and allow the settings are to be absolutely positioned a bit later.
- style.css
#feh-timer-functions {
height: 145px;
position: relative;
}
Now, we can add our buttons. Please note that there are indeed two buttons here, but the pause button is sitting directly behind the play button.
- index.htm
- style.css
- Result
<div id="feh-timer-buttons">
<button id="pause-btn"><i class="fa fa-pause"></i></button>
<button id="start-btn"><i class="fa fa-play"></i></button>
</div>
#feh-timer-buttons {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease-in-out;
}
#feh-timer-buttons button {
background-color: #fd5252;
position: absolute;
border: none;
border-radius: 100px;
cursor: pointer;
width: 60px;
height: 60px;
margin: 0 auto;
display: block;
font-size: 22px;
color: #fff;
}
Our little pomodoro timer is now taking shape. Last elements to add are our settings, then we can get to work and make this thing work.
8. Creating Our Settings Area
As with our buttons area, this settings area are will go inside our #feh-timer-functions
parent.
- index.htm
- style.css
- Result
<div id="feh-timer-settings">
<span class="btn-icon" id="feh-close-settings"><i class="fa fa-times"></i></span>
<form id="feh-timer-form">
<p class="feh-timer-line">
<label for="work-duration">Pomodoro:</label>
<input type="number" id="work-duration" value="25" min="1">
</p>
<p class="feh-timer-line">
<label for="rest-duration">Rest:</label>
<input type="number" id="rest-duration" value="5" min="1">
</p>
</form>
</div>
#feh-timer-settings {
transition: all 0.3s ease-in-out;
/* opacity: 0;
visibility: hidden; */
}
#feh-close-settings {
top: -45px;
height: 80px;
background: #d85e3a;
color: #f7ddd7;
}
#feh-close-settings:hover { color: #fff; }
#feh-timer-form {
border-radius: 20px;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 20px;
background: #d85e3a;
z-index: 1;
}
.feh-timer-line {
display: flex;
}
.feh-timer-line:last-child {
margin-top: 20px;
}
.feh-timer-line * {
font-size: 18px;
}
.feh-timer-line label {
width: 50%;
color: #ffffffc7;
padding: 10px 0px 10px 0px;
}
.feh-timer-line input {
background: #fff;
color: #5c5c5c;
width: 50%;
border: 0px none;
margin-left: -2px;
border-radius: 10px;
text-align: center;
}
#feh-timer-form input::-webkit-outer-spin-button,
#feh-timer-form input::-webkit-inner-spin-button { -webkit-appearance: none; }
#feh-timer-form input[type=number] { -moz-appearance: textfield; }
Notes
All pretty basic stuff here so I won’t delve too deep here, but please note that I commented out the opacity and visibility declarations of the #feh-timer-settings
element to show you how this looks. Please uncommented these now, as these settings will hidden by default.
9. Declaring the Pomodoro Timer’s Primary Variables
We have already declared one variable previously called fehBody
. Let’s declare the main timer’s variables now.
- main.js
const workDurationInput = document.getElementById("work-duration");
const restDurationInput = document.getElementById("rest-duration");
const timerTime = document.getElementById("feh-timer-time");
let workDuration = parseInt(workDurationInput.value) * 60;
let restDuration = parseInt(restDurationInput.value) * 60;
let remainingTime = workDuration;
let isPaused = true;
let isWorking = true;
let intervalId;
Notes
Explanation of these variables below:
workDurationInput
– the input field for the pomodoro (work) input field.restDurationInput
– the input field for the rest input field.circleProgress
– thecircle
element inside our SVG that will animate down.timerTime
– the main timer area inside our SVG circle – 25:00.workDuration
– The value ofworkDurationInput
, converted to an integer and converted to minutes.restDuration
– same as above but used for therestDurationInput
input value.remainingTime
– A copy of theworkDuration
variable.isPaused
– Boolean.isWorking
– Boolean.intervalId
– The heart of the timer.
10. Coding the Start Button Functionality
Let’s now create the functionality to begin the timer when we click on the play button.
- main.js
const startBtn = document.getElementById("start-btn");
startBtn.addEventListener("click", () => {
isPaused = false;
fehBody.classList.add('timer-running');
if (isWorking) {
fehBody.classList.remove('timer-paused');
}
else {
fehBody.classList.add('rest-mode');
fehBody.classList.remove('timer-paused');
}
if (!intervalId) {
intervalId = setInterval(updateTimer, 1000);
}
});
Notes
So we are doing a few things here once we click the startBtn
button.
- We firstly change the
isPaused
variable to false. - We add a class to our body element called
timer-running
. - We use an if/else block to add/remove styles to the
body
element depending on whether the timer is a work timer or rest timer. - The most important part of this code is
intervalId = setInterval(updateTimer, 1000);
This is essentially our timer. We run another function calledupdateTimer()
(we’ll create this next), and update the interval every 1,000 milliseconds, or 1 second.
11. Creating our updateTimer() Function
The updateTimer()
function will perform quite a few actions, but basically it’s decrementing the timer, updating classes on our body element (we’re coming to this part shortly), checking what timer is running; work or rest etc, and stoping the timer if it reaches 0.
- main.js
function updateTimer() {
if (!isPaused) {
remainingTime--;
if (remainingTime <= 0) {
isWorking = !isWorking;
remainingTime = isWorking ? workDuration : restDuration;
if(!isWorking) {
fehBody.classList.add('rest-mode');
fehBody.classList.remove('timer-running');
}
else {
fehBody.classList.remove('rest-mode');
fehBody.classList.remove('timer-running');
}
isPaused = true;
fehBody.classList.remove('timer-work-active');
}
// updateProgress();
console.log(remainingTime);
}
}
So you’d think that our little program would be running now right? I mean, we click the start button and nothing happens! β Well… it is actually running, we just need another function to perform the updates to the UI.
If we add a console.log
message inside our updateTimer()
function, and output our remainingTime
value – we can see the value decrement. Hazaaa!
12. Creating the updateProgress() Function
Our timer is running in the background, let’s now display this on the front-end of our pomodoro timer! Oh but first uncomment the updateProgress();
code in the updateTimer()
body as we’ll be creating this next, and then we can move on.
- main.js
function updateProgress() {
const radius = 45;
const circumference = 2 * Math.PI * radius;
const totalDuration = isWorking ? workDuration : restDuration;
const dashOffset = circumference * remainingTime / totalDuration;
circleProgress.style.strokeDashoffset = dashOffset;
timerTime.textContent = formatTime(remainingTime);
}
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`;
}
// Now run this updateProgress function on load
updateProgress();
Notes
We’ve created the updateProgress()
function, but also created a new function called formatTime()
. Then run the updateProgress();
on page load, as this will update the UI without having to clicking the start button.
Let’s go through what each of the functions are doing.
The updateProgress() Function
We set the radius of our circle, which is just under half of the SVG at 45% (taking into the account the stroke width at 7px), we use this value to get the circumference, using PI, which comes to 282.7433388230814.
Earlier I mentioned that I added 283 as the stroke-dasharray
value. This is where I got that value (rounding up).
Then I get the value of either the work or rest timer values and apply this number to the .circle-progress
element. Then as the timer progresses, the stroke-dashoffset
adds more stroke, making it it look like the circle is progressing.
Last thing here is we update the 25:00 timer value with the updated time that is remaining.
The formatTime() Function
This is really just a small utility function that converts seconds into minutes and seconds.
We convert the integers into strings and then use the padStart
function to add up to two zeros, prefixed, if needed.
13. Show and Hide the Start / Pause Button
Our timer’s running, which is pretty cool, we still have a bit to go to get this ready to show the world. Next up is to toggle the buttons, depending on whether the timer is paused (or stopped) or not.
- main.js
- style.css
const pauseBtn = document.getElementById("pause-btn");
pauseBtn.addEventListener("click", () => {
isPaused = true;
fehBody.classList.remove('timer-running');
fehBody.classList.add('timer-paused');
});
body.timer-running #start-btn {
opacity: 0;
visibility: hidden;
}
body.timer-paused #start-btn {
opacity: 1;
visibility: visible;
}
Notes
You’ll notice in our little program, we are adding and removing classes to the body of our webpage in a few areas, such as when the a start / pause button is pressed, or when the timer has finished. Let’s concentrate on two classes that we’ve been adding and removing:
body.timer-running
body.timer-paused
We can use these classes to toggle the visibility of either the start button or pause button.
What I’ve done in the above code is to show the start button when the pause button has been clicked (or when the timer has finished). And when the pause button has been clicked, show the start button.
14. Add Ability to Show and Hide Settings
You know what would be nice? Allowing the user to control the pomodoro timer settings, instead of forcing 25/5 minute sessions on everybody π
- main.js
- style.css
- Result
const btnToggleSettings = document.getElementById('feh-toggle-settings');
const btnCloseSettings = document.getElementById('feh-close-settings');
function setBodySettings() {
fehBody.classList.contains('settings-active') ? fehBody.classList.remove('settings-active') : fehBody.classList.add('settings-active');
}
function toggleSettings() {
if (event.type === 'click') {
setBodySettings();
}
else if((event.type === 'keydown' && event.keyCode === 27)) {
fehBody.classList.remove('settings-active');
}
}
btnToggleSettings.addEventListener('click', toggleSettings);
btnCloseSettings.addEventListener('click', toggleSettings);
document.addEventListener('keydown', toggleSettings);
body.settings-active #feh-timer-settings {
opacity: 1;
visibility: visible;
}
body.settings-active #feh-timer-sessions {
opacity: 0;
visibility: hidden;
}
body.settings-active #feh-timer-buttons {
opacity: 0;
visibility: hidden;
}
Notes
The above code does a few jobs:
- Toggles a
.settings-active
class to the body if the settings div is opened or not. - Toggles the visibility of the buttons and sessions completed areas.
- Allows the user to close the settings by a few different methods β clicking the close button, pressing the escape key, and of course toggling the settings icon.
15. Updating Work and Rest Times From Settings
Now we have the ability to see the settings area, we just need to be able to update the timer when we change the values from either of these fields.
- main.js
workDurationInput.addEventListener("change", () => {
workDuration = parseInt(workDurationInput.value) * 60;
if (isWorking) {
remainingTime = workDuration;
updateProgress();
}
});
restDurationInput.addEventListener("change", () => {
restDuration = parseInt(restDurationInput.value) * 60;
if (!isWorking) {
remainingTime = restDuration;
updateProgress();
}
});
The above events run on change of value of either the workDurationInput
or restDurationInput
inputs. Then the timer is updated in realtime.
16. Make the Complete Work Sessions Update
Time to start counting our work sessions, so let’s navigate back to our updateTimer()
function, and add some code in.
- main.js
const completedSessionsElement = document.getElementById("feh-completed-sessions");
let completedSessions = 0;
function updateTimer() {
if (!isPaused) {
if (remainingTime <= 0) {
if(!isWorking) {
completedSessions++;
completedSessionsElement.textContent = completedSessions;
}
}
}
}
We are creating two new variables, one to target the counter area on the front-end #feh-completed-sessions
, and a counter completedSessions
to keep track of how many work sessions we have completed.
Now when we load the page, the counter increments nicely.
17. Adding Music When Each Timer Completes
I’ve seen this on a few websites with online timers and I think it’s pretty nifty β a sound plays when the timer completes. We’ll add this in. We’re going to play two different sounds, depending on which timer completes. I have added the sounds from Pixabay.com.
Again, going back to our updateTimer()
, let’s add some new code inside.
- main.js
function updateTimer() {
const workFinished = new Audio("/music/success-fanfare-trumpets-6185.mp3");
const restFinished = new Audio("/music/error-when-entering-the-game-menu-132111.mp3");
if (remainingTime <= 0) {
playAlarm = isWorking ? restFinished : workFinished;
playAlarm.play();
}
}
18. Adding Pomodoro Timer to Browser Tab
It would be helpful to still see the Pomodoro countdown timer if we navigate away from the web page. Again, popping back into our updateTimer() function, we’ll add this in near the bottom.
- main.js
- Result
if (!isPaused) {
...
document.title = timerTime.textContent = formatTime(remainingTime);
...
}
19. Adding Some Status Text
Another little enhancement here, when the timer is either paused or in rest mode, I think it would be cool to show this text. We already have the text elements inside of our SVG ready to go, and our body classes are updating with what timer is running or if it is paused, so let’s add some CSS to toggle this area.
- main.js
- Result
body.timer-paused:not(.rest-mode) #feh-timer-pause,
body.rest-mode #feh-timer-rest,
body.rest-mode.timer-paused #feh-timer-pause {
opacity: 1;
}
body.rest-mode.timer-paused #feh-timer-rest {
opacity: 0;
}
Above, we’re just showing and hiding the pause / rest text based on a few factors, such as if the timer is paused, if the timer is in rest mode, if the timer is in rest mode and paused etc.
20. Dim Background Image & Progress Bar In Rest Mode
Last feature to add here π Again, just another ‘nice to have’ but I think it adds to the overall project.
When the work timer reaches 0 and enters rest mode, we’re going to dim and blur the background image, and make the progress stroke line a more muted colour. Again, we already have the class names we need in the body element to achieve this.
- style.css
- Result
body.rest-mode #vista-bg {
filter: blur(5px) grayscale(0.6) brightness(0.5);
}
body.rest-mode .circle-progress {
stroke: #fd525385;
}
21. Circle Progress Line Clean up
All features have now been added, I’d just like to clean up the circular animation to be more smooth. But we don’t want the .circle-progress
to have this transition rule all of the time, as it will look odd when the timer finishes β please see below.
We only want the transition to happen when the timer is running.
- style.css
body.timer-running .circle-progress {
transition: all 1s linear;
}
22. Conclusion & Code
And that’s a wrap! We now have a pretty feature packed pomodoro timer made with HTML, CSS & JavaScript. You can play around with it and add more features if you’d like? Maybe add a play / pause favicon?
If you made it this far, thanks very much for reading along and I hope this was useful. Any comments or anything like that, please let me know. Code can be found here.