Build a Pomodoro Study Timer
Build a dark-themed focus timer with HTML, CSS, and JavaScript from scratch.
Introduction
30 Second Summary
Studying for hours without a plan is a recipe for burnout. The Pomodoro technique fixes that by splitting your work into focused 25-minute sessions with short breaks in between.
In this project, you will build a calming, dark-themed study timer called Focus Flow from scratch. You will write every line of code yourself across three files using HTML, CSS, and JavaScript.
What You'll Build
You type what you're studying, press Start, and watch a 25-minute countdown tick down second by second, switching to a calming break mode with a purple-to-teal color shift when it hits zero.
By the end of this project, you'll have:
- A live countdown timer that ticks down second by second, automatically switching between 25-minute focus and 5-minute break modes with a calming color shift.
- A task display where you type what you're studying and see it appear on the timer card while you work.
- A session counter that remembers how many focus sessions you've completed, even after closing and reopening your browser.
- Secret Mission: Add a session history log that displays every completed focus session with its task name and timestamp.
Do I need any coding experience?
No previous coding experience is needed. Every tool is free, and setup is fully covered in Step 1.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Workspace
Before your code can come to life in a browser, you need a way to preview it instantly as you type. Live Server is a VS Code extension that refreshes your browser every time you save a file.
In this step, you'll install Live Server, create a project folder with three separate files, and write the HTML boilerplate that connects them all together.
In this step, get ready to:
- Install the Live Server extension in VS Code.
- Create your project folder and three files.
- Connect the files together with HTML boilerplate.
Install the Live Server extension
When you write code and save the file, you normally have to refresh the browser manually to see your changes. Live Server eliminates that step by watching your files and refreshing the browser for you automatically.
- Press Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows) to open the Extensions view in VS Code.
- Type Live Server in the search bar at the top.
- Find the result by Ritwick Dey and click Install.
The Install button changes to show Uninstall, confirming the extension is ready to use.
Don't see Live Server in the results?
Make sure you typed Live Server as two words. There are several extensions with similar names. Look for the one by Ritwick Dey specifically.
Help me find and install the Live Server extension in VS Code.
Create your project folder and files
Your Focus Flow app will live in three separate files. index.html handles structure, style.css handles styling (CSS), and script.js handles behavior (JavaScript).
- Click Terminal in the menu bar at the top of VS Code.
- Click New Terminal.
A terminal panel appears at the bottom of VS Code.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder, move into it, and create your three files by running these commands:
mkdir focus-flow
cd focus-flow
touch index.html style.css script.js
Windows users
If touch is not recognized, run this PowerShell alternative instead:
New-Item -ItemType File index.html, style.css, script.js
- Confirm the files exist by running:
ls
You should see index.html, script.js, and style.css in the output.
Don't see the three files?
Make sure you ran cd focus-flow before running ls. If you are still in the Desktop directory, you will see the focus-flow folder itself, not its contents.
Help me create my project folder and files in the terminal.
Now open this folder in VS Code so you can see and edit the files.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the focus-flow folder, then click Open.
Seeing a trust prompt?
VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue. This is a security feature that lets VS Code run code in this folder.
VS Code's Explorer sidebar now shows your three files: index.html, style.css, and script.js.
Connect the three files together
The browser loads index.html first. HTML is the skeleton that tells the browser what to show on the page. It also tells the browser where to find the CSS and JavaScript files.
- Click on index.html in the Explorer sidebar to open it.
- Add the HTML boilerplate that links your three files together by typing this code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Focus Flow</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="script.js"></script>
</body>
</html>
What does this code do?
- <!DOCTYPE html> tells the browser this is an HTML5 document.
- <title>Focus Flow</title> sets the text that appears on the browser tab.
- <link rel="stylesheet" href="style.css"> loads your CSS file so the browser can apply your styles.
- <script src="script.js"></script> loads your JavaScript file so the browser can run your code.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Right-click index.html in the Explorer sidebar (not in the editor tab).
- Select Open with Live Server.
Before you look at the browser, what do you think you'll see? You haven't added any visible content inside the body tags yet.
Your browser opens to a blank white page. Check the tab at the top of the browser. It reads "Focus Flow". That blank page is your app running live. Every change you save from here will appear instantly.
Browser didn't open?
Make sure you right-clicked index.html in the Explorer sidebar on the left, not in the editor area. The right-click menu in the sidebar has the Open with Live Server option.
You can also click the Go Live button in VS Code's bottom status bar. If you don't see it, double check that the Live Server extension is installed.
Help me launch Live Server to preview my HTML file in the browser.
✔️ Awesome, I've got everything!
Double check you've saved index.html. The white dot on the editor tab disappears when the file is saved.
ⓧ I'd like to double check the full code
Here is the complete index.html at this point. Make sure yours matches exactly.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Focus Flow</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="script.js"></script>
</body>
</html>
Your workspace is ready and Live Server is watching every save. Next up, you'll fill that blank page with the HTML elements that make up your timer.
Build the Timer Structure with HTML
Your workspace is set up and Live Server is serving a blank page in your browser. Time to put something on that page.
Every web app starts as a skeleton of HTML elements. HTML defines what appears on the page: the headings, inputs, buttons, and text. Without it, there is nothing to style or make interactive.
In this step, get ready to:
- Add a container wrapper and the app title.
- Add the task input, timer display, buttons, and session counter.
Add the container and header
Every element in your app needs a wrapper to live inside. The container div groups everything together so you can center and size the whole app later with CSS.
- In index.html, find the blank line between the opening <body> tag and the <script> tag.
- Add the container wrapper and app title by pasting this code on that blank line:
<div class="container">
<h1 class="app-title">Focus Flow</h1>
</div>
What does this code do?
- <div class="container"> creates a wrapper that will hold every visible element in the app. The class attribute gives CSS a name to target when styling it later.
- <h1> is the largest heading tag in HTML. It creates the app's main title on the page.
- Save index.html.
- Check your browser. You should see "Focus Flow" as a large heading on a white page.
Don't see the heading?
- Make sure you pasted the code inside the <body> tag, above the <script src="script.js"></script> line.
- Check that Live Server is still running. Look for "Port: 5500" in the VS Code status bar at the bottom.
- Why isn't my heading showing in the browser?
Add the task input, timer display, controls, and session counter
The heading is on the page, but a timer needs many more pieces: a text input, a countdown display, buttons, and a session counter. Each element gets an id so JavaScript can find and control it later, and a class so CSS can style it.
- In index.html, find the line that says <h1 class="app-title">Focus Flow</h1>.
- Add the following code below that line, but still inside the .container div (above its closing </div> tag):
<div class="task-section">
<label for="task-input">What are you studying?</label>
<input type="text" id="task-input" placeholder="e.g. Chapter 5 Biology">
</div>
<div class="timer-card">
<p class="mode-label" id="mode-label">Focus Time</p>
<p class="timer-display" id="timer-display">25:00</p>
<p class="task-display" id="task-display"></p>
</div>
<div class="controls">
<button class="btn btn-start" id="start-btn">Start</button>
<button class="btn btn-reset" id="reset-btn">Reset</button>
</div>
<div class="session-info">
<p>Sessions completed: <span id="session-count">0</span></p>
</div>
What does this code do?
- The .task-section contains a label and a text input where you type what you are studying. The id="task-input" lets JavaScript read the typed value later.
- The .timer-card holds three paragraphs: the mode label ("Focus Time"), the countdown display ("25:00"), and a task display area that JavaScript will fill in while the timer runs.
- The .controls div contains two buttons. Each has an id so JavaScript can attach click behavior to them.
- The <span id="session-count"> wraps just the number inside the session info paragraph. This lets JavaScript update the count without touching the surrounding text.
What is an id vs a class?
An id is a unique name for one specific element. JavaScript uses it to grab that exact element from the page. Each id should only appear once in the entire file.
A class is a reusable label for styling. Multiple elements can share the same class, and CSS uses classes to apply the same look to all of them.
Before you check your browser, take a guess: what do you think the page looks like with all these elements but zero CSS styling?
- Save index.html.
- Check your browser. You should see "Focus Flow" as a heading, a text input, "Focus Time", "25:00", two buttons labeled "Start" and "Reset", and "Sessions completed: 0".
It looks like an unstyled document from the 1990s, but every piece of your app is now on the page.
Missing some elements?
- Make sure all the new code is inside the <div class="container"> wrapper, between the <h1> line and the closing </div> tag.
- Check for missing closing tags. Every <div> needs a matching </div>, and every <p> needs a matching </p>.
- Why are some of my HTML elements missing from the page?
✔️ Awesome, I've got everything!
Great. Double check you have saved index.html before moving on.
ⓧ I'd like to double check the full code
Your index.html should contain these sections in order. Scroll up and confirm each one is present:
- The <!DOCTYPE html> boilerplate with <head>, the style.css link, and opening <body> tag (from Step 1).
- The <div class="container"> wrapper with the <h1 class="app-title"> heading (Chunk 1).
- The .task-section, .timer-card, .controls, and .session-info divs all inside the container, followed by the closing </div> (Chunk 2).
- The <script src="script.js"></script> line before the closing </body> tag (from Step 1).
Every piece of your timer is on the page. Next up, you will transform this bare HTML into a professional dark-themed interface with CSS.
Style the App with CSS
Your app has every piece in the browser. The heading, input, buttons, and session counter are all there. They just look like a plain text document.
CSS transforms raw HTML into a polished interface. In this step, you'll define a calming dark color palette and apply it to every element, turning your plain skeleton into something that looks like a real app.
In this step, get ready to:
- Define a color palette using CSS custom properties.
- Style the timer card, input field, and buttons with a dark theme.
- Discover what CSS alone cannot do.
Set up CSS custom properties and base styles
CSS custom properties let you store values like colors in one place and reuse them everywhere. When you want to tweak a color later, you change it once instead of hunting through every rule.
- Open style.css in VS Code and add your color palette, browser reset, and dark background by pasting this code:
/* Color palette stored as reusable variables */
:root {
--bg-color: #1a1b2e;
--card-bg: #252642;
--text-color: #e2e4f0;
--text-muted: #8b8da3;
--focus-color: #7c6fe0;
--break-color: #4ecdc4;
--btn-bg: #7c6fe0;
--btn-hover: #6b5dd3;
--btn-reset-bg: #3a3b5c;
--btn-reset-hover: #4a4b6c;
--border-radius: 16px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
What are CSS custom properties?
The :root block defines variables like --bg-color that you can reference anywhere with var(--bg-color). This means every color in your app points back to one central list.
The * selector resets default browser spacing on every element. The body styles apply the dark background and use flexbox to center your entire app on screen.
- Save style.css and check your browser.
- The page is now deep navy blue with light text, and your content is centered vertically and horizontally.
Still seeing a white background?
- Make sure you saved style.css. Unsaved files show a dot next to the filename in VS Code's tab bar.
- Open index.html and confirm the <link> tag says href="style.css". A typo here means the browser never loads your styles.
- Confirm Live Server is still running. You should see a port number (like 5500) in VS Code's status bar.
Why isn't my CSS loading in the browser?
Style the task input, timer card, buttons, and session info
Your dark background is set. Now you'll shape every component on the page, starting with the layout container and task input area.
- Add the container, title, and task section styles below the existing code in style.css:
.container {
width: 100%;
max-width: 400px;
padding: 24px;
text-align: center;
}
.app-title {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 32px;
}
/* Spacing below the task input area */
.task-section {
margin-bottom: 32px;
}
/* Muted label above the text input */
.task-section label {
display: block;
font-size: 0.875rem;
color: var(--text-muted);
margin-bottom: 8px;
}
What does this code do?
- .container caps the app width at 400px and centers the content. This keeps the layout clean on any screen size.
- .task-section label uses var(--text-muted) to pull the muted gray color from your custom properties. Every color reference uses var() to stay consistent.
- Save style.css and check your browser.
- The title is centered with spacing below it. The label text above the input is now a smaller, muted gray.
- Style the text input field by adding this code below what you just wrote:
/* Dark rounded input field */
.task-section input {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--card-bg);
border-radius: var(--border-radius);
background-color: var(--card-bg);
color: var(--text-color);
font-size: 1rem;
outline: none;
transition: border-color 0.3s;
}
/* Purple glow when the input is clicked */
.task-section input:focus {
border-color: var(--focus-color);
}
.task-section input::placeholder {
color: var(--text-muted);
}
What's happening here?
- The transition: border-color 0.3s property creates a smooth animation when the border changes color. Without it, the purple glow would snap on and off instantly.
- The :focus selector targets the moment a learner clicks into the input. The border smoothly shifts to the purple --focus-color to show the input is active.
- Save style.css and check your browser.
- The input now has a dark background and rounded corners that blend into the page. Click inside it to see the purple border glow animate in.
Input still looks like a plain white box?
- Check that your selector says .task-section input with a space between them. The space means "an input inside the task-section class." Without it, CSS looks for an element called task-sectioninput, which doesn't exist.
- Make sure every property line ends with a semicolon. A missing semicolon can silently break every line below it.
Why isn't my input field picking up the dark CSS styles?
- Style the timer card and its contents by adding this code below the input styles:
/* Rounded card that holds the timer */
.timer-card {
background-color: var(--card-bg);
border-radius: var(--border-radius);
padding: 40px 24px;
margin-bottom: 24px;
}
.mode-label {
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--focus-color);
margin-bottom: 16px;
font-weight: 600;
}
.timer-display {
font-size: 4.5rem;
font-weight: 700;
font-variant-numeric: tabular-nums; /* Fixed-width digits prevent jitter */
line-height: 1;
margin-bottom: 8px;
}
.task-display {
font-size: 0.875rem;
color: var(--text-muted);
min-height: 1.25rem;
}
What does this code do?
- .timer-card creates a slightly lighter dark card that visually separates the countdown from the background.
- .mode-label uses text-transform: uppercase and letter-spacing to make "Focus Time" appear as a small, spaced-out heading in purple.
- font-variant-numeric: tabular-nums forces every digit to take the same width. Without it, the timer display would jitter left and right as digits change (a "1" is narrower than a "0" in most fonts).
- Save style.css and check your browser.
- You should see a rounded dark card with "FOCUS TIME" in purple uppercase and a large "25:00" display below it.
- Add the button layout and shared button styles:
/* Button layout: side by side */
.controls {
display: flex;
gap: 12px;
margin-bottom: 32px;
}
/* Shared button styles */
.btn {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: var(--border-radius);
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.3s;
}
What does this code do?
- display: flex on .controls places the Start and Reset buttons side by side instead of stacking them vertically.
- flex: 1 on .btn makes both buttons share the available width equally.
- Save style.css and check your browser.
- The Start and Reset buttons now sit side by side with rounded corners and consistent sizing. They don't have their final colors yet.
- Give each button its own color scheme by adding these styles:
/* Purple Start button */
.btn-start {
background-color: var(--btn-bg);
color: white;
}
.btn-start:hover {
background-color: var(--btn-hover);
}
/* Dark gray Reset button */
.btn-reset {
background-color: var(--btn-reset-bg);
color: var(--text-color);
}
.btn-reset:hover {
background-color: var(--btn-reset-hover);
}
What's happening here?
Each button gets its own background color. The :hover selectors define what color the button shifts to when the learner moves their mouse over it. This subtle feedback tells the user "this is clickable."
- Save style.css and check your browser.
- Start is now a bold purple button. Reset is a subtle dark gray. Hover over each to see the color shift smoothly.
- Finish the stylesheet by adding the session counter styles and break-mode color overrides:
.session-info {
color: var(--text-muted);
font-size: 0.875rem;
}
.session-info span {
color: var(--text-color);
font-weight: 600;
}
/* These styles activate when JavaScript adds the break-mode class */
body.break-mode .mode-label {
color: var(--break-color);
}
body.break-mode .btn-start {
background-color: var(--break-color);
}
body.break-mode .btn-start:hover {
background-color: #3dbdb5;
}
body.break-mode .task-section input:focus {
border-color: var(--break-color);
}
What are the break-mode styles for?
The body.break-mode selectors only apply when the <body> element has a class called break-mode. Right now, it doesn't have that class, so these rules sit dormant.
Later, JavaScript will add and remove this class to swap the accent color from purple to teal when the timer switches to break mode. The CSS is ready and waiting.
- Save style.css and check your browser.
- The session counter text is now muted gray with the number in bold white. Your stylesheet is complete.
✔️ Awesome, I've got everything!
Great. Double check you've saved style.css before moving on.
ⓧ I'd like to double check the full code
Scroll up through your work and confirm each section is present in this order:
- :root variables, * reset, and body styles.
- .container, .app-title, and .task-section styles (including label).
- .task-section input styles (including :focus and ::placeholder).
- .timer-card, .mode-label, .timer-display, and .task-display styles.
- .controls and .btn shared styles.
- .btn-start and .btn-reset color schemes (including :hover variants).
- .session-info styles and all four body.break-mode overrides.
If anything is missing, scroll up to the matching code block and paste it into the correct spot in your file.
Your app has transformed from a plain document into a professional dark-themed interface. Before you click Start, take a guess: what do you think will happen?
- Click the Start button.
Nothing. The button does absolutely nothing.
Why don't the buttons work?
HTML creates structure. CSS creates appearance. But neither one creates behavior. Clicking a button, counting down a timer, pausing, resetting: all of that requires JavaScript.
Your app is a beautiful shell with no brain. JavaScript is the missing piece that brings everything to life.
Something still looks off?
- If a specific element looks unstyled, open your HTML file and confirm the class name in the HTML (e.g. class="timer-card") matches the CSS selector exactly (.timer-card). Class names are case-sensitive.
- Check for missing closing braces }. A single missing brace can break every rule that comes after it.
- Right-click the broken element in your browser and select Inspect. The styles panel shows which CSS rules are applied, and crossed-out properties mean something is overriding them.
My Focus Flow app doesn't look right after adding all the CSS styles. Help me figure out what's wrong.
Your app looks polished, but those buttons need a brain. In the next step, you'll write JavaScript that makes the timer count down, pause, and reset.
Bring the Timer to Life with JavaScript
Your app looks polished. The dark theme, the rounded card, the purple buttons. But try clicking Start. Nothing happens.
HTML creates the structure of your page. CSS creates the style. JavaScript is the third piece: it creates behavior. In this step, you'll write the JavaScript that makes your timer count down, pause, and reset.
In this step, get ready to:
- Connect JavaScript to the HTML elements on your page.
- Build a countdown that ticks down second by second.
- Add start, pause, and reset controls.
Grab references to your HTML elements and set up timer variables
JavaScript needs a way to find each element on your page. Every element you gave an id attribute to in your HTML can be grabbed by JavaScript using getElementById. Once grabbed, JavaScript can read it, change it, or respond when a user clicks it.
- In VS Code, click on script.js in the Explorer sidebar to open it.
- Add the DOM references and timer variables by pasting this code:
// Grab references to HTML elements by their id
const timerDisplay = document.getElementById('timer-display');
const startBtn = document.getElementById('start-btn');
const resetBtn = document.getElementById('reset-btn');
// 25 minutes in seconds
const FOCUS_DURATION = 25 * 60;
// Timer state variables
let timeRemaining = FOCUS_DURATION;
let timerInterval = null;
let isRunning = false;
What does this code do?
- document.getElementById('timer-display') reaches into your HTML and grabs the element with id="timer-display". Now the variable timerDisplay holds a direct reference to that element, so JavaScript can change its text or respond to clicks.
- FOCUS_DURATION stores 25 minutes as 1500 seconds (25 * 60). Working in seconds makes the countdown math simpler.
- The three let variables track the timer's current state: how many seconds are left, whether an interval is actively ticking, and whether the timer is running or paused.
- Save script.js.
- Switch to your browser. Right-click anywhere on the page and select Inspect.
- Click the Console tab. You should see an empty console with no red error messages.
Why check the console?
The browser console is where JavaScript reports problems. A clean console (no red text) means your code loaded without syntax errors. If you see red error messages, that means JavaScript found something it could not understand in your file.
Seeing a red error in the console?
- Check for missing semicolons or unclosed quotes in script.js. A single missing character can break the whole file.
- Make sure you saved the file. Unsaved changes won't load in the browser.
- Confirm your index.html still has <script src="script.js"></script> just before the closing </body> tag.
Why is my script.js causing a console error?
Write the countdown and control functions
Your variables are set up, but they don't do anything on their own. You need functions that use those variables to update the display, count down each second, and respond to button clicks.
- In script.js, add the display and countdown functions below your variables:
// Convert seconds to MM:SS and update the display
function updateDisplay() {
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
timerDisplay.textContent =
String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
}
// Subtract one second and check if time is up
function tick() {
timeRemaining--;
updateDisplay();
if (timeRemaining <= 0) {
clearInterval(timerInterval);
timerInterval = null;
isRunning = false;
startBtn.textContent = 'Start';
}
}
What do these functions do?
- updateDisplay() takes timeRemaining (a number in seconds) and converts it to the MM:SS format you see on screen. padStart(2, '0') adds a leading zero so you get 05:03 instead of 5:3.
- tick() subtracts one second from timeRemaining, then calls updateDisplay() to show the new time. When the timer reaches zero, it stops the countdown by calling clearInterval.
- Save script.js. You won't see a visible change in the browser yet. The next chunk wires these functions to the buttons.
- Add the start/pause function, reset function, event listeners, and initial display call below your tick function:
// Toggle the timer between running and paused
function startTimer() {
if (isRunning) {
isRunning = false;
startBtn.textContent = 'Start';
clearInterval(timerInterval);
timerInterval = null;
return;
}
isRunning = true;
startBtn.textContent = 'Pause';
timerInterval = setInterval(tick, 1000);
}
// Stop the timer and reset everything to 25:00
function resetTimer() {
clearInterval(timerInterval);
timerInterval = null;
isRunning = false;
startBtn.textContent = 'Start';
timeRemaining = FOCUS_DURATION;
updateDisplay();
}
// Wire button clicks to functions
startBtn.addEventListener('click', startTimer);
resetBtn.addEventListener('click', resetTimer);
// Show 25:00 on first load
updateDisplay();
What does this code do?
- startTimer() does double duty. If the timer is already running, it pauses by clearing the interval. If the timer is paused, it starts by calling setInterval(tick, 1000), which runs the tick function once every 1000 milliseconds (one second).
- resetTimer() stops any running countdown, resets timeRemaining back to 25 minutes, and refreshes the display.
- addEventListener('click', startTimer) tells the browser: "when someone clicks this button, run this function." This is the line that connects your JavaScript logic to the HTML buttons you built earlier.
- The final updateDisplay() call runs once when the page first loads, so the timer shows 25:00 right away.
- Save script.js.
Before you click Start in the browser, predict: will the timer count down, or will clicking still do nothing?
- Click Start in your browser.
The timer counts down from 25:00. The button text changes to Pause. Click it again and the countdown freezes. Click Reset and it jumps back to 25:00. The buttons are alive.
Timer not counting down?
- Make sure you saved script.js. Unsaved changes won't show in the browser.
- Open the browser console (right-click, Inspect, Console tab) and check for red error messages. A typo in a function name or a missing bracket will show up here.
- Verify that the id values in your getElementById calls match the id attributes in your HTML exactly. For example, start-btn in JavaScript must match id="start-btn" in HTML.
Why isn't my timer responding to button clicks?
✔️ Awesome, I've got everything!
Great. Make sure you've saved script.js before continuing.
ⓧ I'd like to double check the full code
Your script.js has four sections. Scroll up and confirm each one is present in this order:
- DOM references and timer variables at the top: three const lines using getElementById, one FOCUS_DURATION constant, and three let variables.
- updateDisplay and tick functions that convert seconds to MM:SS and count down each second.
- startTimer and resetTimer functions that handle button clicks.
- Two addEventListener lines and a final updateDisplay() call at the bottom.
The countdown works, pausing works, and resetting works. But what happens when the timer actually reaches 00:00? You don't want to wait 25 minutes to find out.
Speed up testing
You can temporarily shorten the timer duration to 5 seconds so the countdown finishes in seconds instead of minutes. You'll change it back after testing.
- In script.js, find this line near the top of your file:
const FOCUS_DURATION = 25 * 60;
- Replace 25 * 60 with 5, so the line now looks like this:
const FOCUS_DURATION = 5;
- Save script.js.
- In the browser, click Reset to pick up the new duration, then click Start.
The timer counts down from 00:05. Before it reaches zero, predict: what do you think will happen when it hits 00:00?
It just stops. The button changes back to Start, and that's it. No break mode. No session tracking. Refreshing the page resets everything. The timer counts down, but it dead-ends at zero.
- Change FOCUS_DURATION back to 25 * 60 and save script.js.
const FOCUS_DURATION = 25 * 60;
Your timer counts, pauses, and resets. That's real JavaScript behavior powering real HTML elements. Next up, you'll turn that dead-end at zero into an automatic focus-to-break cycle that tracks your sessions.
Add Mode Switching and Persistent Sessions
Your timer counts down, pauses, and resets on command. The buttons are alive and the countdown works.
But when the timer hits zero, it just stops. A real Pomodoro timer alternates between focus sessions and breaks, shows what you're studying, and remembers how many sessions you've completed. This step turns your countdown into a complete Pomodoro experience.
In this step, get ready to:
- Expand your variables to connect JavaScript to the remaining HTML elements.
- Add automatic mode switching between focus and break.
- Persist your session count so it survives page refreshes.
Expand your variables and DOM references
Right now your JavaScript only knows about three elements on the page: the timer display and two buttons. The rest of your HTML (the mode label, task input, task display, session counter) is invisible to your code.
- In script.js, find the variable block at the top of the file. It starts with const timerDisplay and ends with let isRunning = false;.
- Find this block:
const timerDisplay = document.getElementById('timer-display');
const startBtn = document.getElementById('start-btn');
const resetBtn = document.getElementById('reset-btn');
const FOCUS_DURATION = 25 * 60;
let timeRemaining = FOCUS_DURATION;
let timerInterval = null;
let isRunning = false;
- Select all those lines and replace them with this expanded version:
const timerDisplay = document.getElementById('timer-display');
const modeLabel = document.getElementById('mode-label');
const taskInput = document.getElementById('task-input');
const taskDisplay = document.getElementById('task-display');
const startBtn = document.getElementById('start-btn');
const resetBtn = document.getElementById('reset-btn');
const sessionCount = document.getElementById('session-count');
const FOCUS_DURATION = 25 * 60;
const BREAK_DURATION = 5 * 60;
let timeRemaining = FOCUS_DURATION;
let timerInterval = null;
let isRunning = false;
let isFocusMode = true;
let sessions = parseInt(localStorage.getItem('focusFlowSessions')) || 0;
sessionCount.textContent = sessions;
What's new here?
- Four new getElementById calls (modeLabel, taskInput, taskDisplay, sessionCount) connect JavaScript to the remaining HTML elements you built in Step 2.
- BREAK_DURATION sets the break length to 5 minutes (300 seconds).
- isFocusMode tracks whether the timer is in focus or break mode.
- sessions loads the saved session count from the browser's localStorage. The || 0 fallback handles the very first visit when nothing is saved yet.
- Save script.js.
- Click Start in your browser. The timer still counts down from 25:00. The new variables are in place but no function uses them yet, so everything works exactly as before.
Timer not counting down?
- Check that you replaced the entire old variable block instead of pasting the new one below the old one. Having duplicate const timerDisplay lines causes an error.
- Open the browser's Developer Console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows) and look for red error messages pointing to the duplicate.
- Help me debug my variable replacement in script.js.
Add mode switching and update the timer functions
The variables are ready. Now you need a switchMode function that flips between focus and break, plus updated versions of tick, startTimer, and resetTimer that use it.
- In script.js, scroll down to the updateDisplay function. Add this new switchMode function directly below updateDisplay (above the existing tick function):
function switchMode() {
if (isFocusMode) {
// Focus session just ended - save progress
sessions++;
localStorage.setItem('focusFlowSessions', sessions);
sessionCount.textContent = sessions;
// Switch to break mode
isFocusMode = false;
timeRemaining = BREAK_DURATION;
modeLabel.textContent = 'Break Time';
document.body.classList.add('break-mode');
} else {
// Break is over - switch back to focus
isFocusMode = true;
timeRemaining = FOCUS_DURATION;
modeLabel.textContent = 'Focus Time';
document.body.classList.remove('break-mode');
}
updateDisplay();
clearInterval(timerInterval);
timerInterval = null;
isRunning = false;
startBtn.textContent = 'Start';
}
What does switchMode do?
When a focus session ends, it increments the session count, saves it to localStorage, sets the timer to 5 minutes, and adds the break-mode CSS class. That class triggers the teal color scheme you styled in Step 3.
When a break ends, it flips back to focus mode and removes the break-mode class. Either way, it stops the interval and resets the button so you can start the next session manually.
This function is ready, but nothing calls it yet. The next chunk updates tick and startTimer to use it.
- Find the existing tick and startTimer functions (right below where you just added switchMode). Select both functions and replace them with these updated versions:
function tick() {
timeRemaining--;
updateDisplay();
if (timeRemaining <= 0) {
// Timer hit zero - switch modes
switchMode();
alert(isFocusMode ? 'Break is over! Ready to focus?' : 'Great work! Time for a break.');
}
}
function startTimer() {
if (isRunning) {
// Already running - pause it
isRunning = false;
startBtn.textContent = 'Start';
clearInterval(timerInterval);
timerInterval = null;
return;
}
// Show the task name on the timer card
const task = taskInput.value.trim();
if (isFocusMode && task) {
taskDisplay.textContent = 'Studying: ' + task;
}
isRunning = true;
startBtn.textContent = 'Pause';
timerInterval = setInterval(tick, 1000);
}
What changed in tick and startTimer?
- tick now calls switchMode() when the timer reaches zero instead of just stopping. After switching, it shows an alert telling you whether it's time for a break or time to focus.
- startTimer now reads the task input field and displays "Studying: [your task]" on the timer card when you start a focus session.
- Save script.js.
- Type Chapter 5 Biology in the task input and click Start. Below the timer, you see "Studying: Chapter 5 Biology" appear on the timer card.
Don't see your task name?
- Make sure you typed something in the input field before clicking Start. If the input is empty, the task display stays blank.
- Check that you replaced the old startTimer function completely, including the closing }. A leftover brace from the old version causes a syntax error.
- Help me debug why the task display isn't appearing.
- Find the existing resetTimer function (the last function before the addEventListener lines). Replace it with this updated version:
function resetTimer() {
clearInterval(timerInterval);
timerInterval = null;
isRunning = false;
// Reset everything back to focus mode
isFocusMode = true;
timeRemaining = FOCUS_DURATION;
modeLabel.textContent = 'Focus Time';
taskDisplay.textContent = '';
document.body.classList.remove('break-mode');
startBtn.textContent = 'Start';
updateDisplay();
}
What's different in resetTimer?
The original reset only cleared the interval and reset the time. This version also resets the mode to focus, clears the task display, removes the break-mode CSS class, and restores the "Focus Time" label. It's a full clean slate.
- Save script.js.
Time to test the full Pomodoro cycle. To avoid waiting 25 minutes, you'll temporarily shorten the focus duration to 5 seconds.
- In script.js, find this line near the top:
const FOCUS_DURATION = 25 * 60;
- Change 25 * 60 to 5 so the line looks like this:
const FOCUS_DURATION = 5;
- Save script.js.
Before you click Start, what do you think will happen when the 5-second timer reaches 00:00? Will the page change color? Will the session count update?
- Type a task name in the input field and click Start.
- Watch the timer count down from 00:05.
When it hits 00:00, an alert pops up saying "Great work! Time for a break." The mode label changes to "BREAK TIME" in teal. The Start button turns teal. The session count changes to 1.
- Dismiss the alert and refresh the page.
The session count still shows 1. Your progress survived the refresh because localStorage saved it.
- In script.js, change FOCUS_DURATION back to 25 * 60 so the line reads const FOCUS_DURATION = 25 * 60; again.
- Save script.js.
Timer not switching modes?
- Make sure the switchMode function is placed between updateDisplay and tick. If tick tries to call switchMode before it's defined, your browser may throw an error.
- Check that you set FOCUS_DURATION to 5 (not '5' in quotes). A string value causes the countdown math to break.
- Help me debug why mode switching isn't working.
✔️ Awesome, I've got everything!
Great. Double check you've saved script.js with FOCUS_DURATION set back to 25 * 60.
ⓧ I'd like to double check the full code
Your script.js should contain these sections in order. Scroll through your file and confirm each one is present:
- Seven const DOM reference lines (timerDisplay through sessionCount), followed by FOCUS_DURATION = 25 * 60 and BREAK_DURATION = 5 * 60, then five let variables, then sessionCount.textContent = sessions;
- updateDisplay function (unchanged from Step 4).
- switchMode function (new in this step).
- tick function (updated to call switchMode and alert).
- startTimer function (updated to read task input).
- resetTimer function (updated to clear mode, task display, and break-mode class).
- Two addEventListener lines and a final updateDisplay(); call (unchanged from Step 4).
Your Pomodoro timer is complete. It counts down, switches between focus and break modes, displays what you're studying, changes color to match the mode, and remembers your progress across page refreshes. You built the entire thing from scratch across three files.
Secret mission
Add a Session History Log
Your timer tracks how many sessions you've completed, but that number tells you nothing about what you studied or when. What if you could see a log of every focus session with its task name and completion time? This mission puts all your HTML, CSS, and JavaScript skills together to build that feature from scratch.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your computer, so there are no ongoing costs. Decide whether to keep your project files, pause by stopping Live Server, or delete everything entirely.
Resources you used:
- focus-flow project folder on your Desktop (containing index.html, style.css, and script.js).
- Live Server extension in VS Code.
- localStorage data saved by your app in the browser (focusFlowSessions and focusFlowHistory).
Keep everything running
No action needed. Choose this if you plan to keep building on Focus Flow or want to revisit the code later.
- Your focus-flow folder stays on your Desktop, ready to open anytime.
- The Live Server extension stays installed in VS Code for future projects.
- Your session count and history remain saved in your browser's localStorage.
Pause - I'll come back to this later
Stop Live Server to free up the port, but keep your files and extension for later.
- In VS Code, click the Port: 5500 button in the bottom status bar to stop Live Server. You can also press Cmd+L, Cmd+C (macOS) to stop it.
- Close the browser tab showing your timer.
- Your focus-flow folder, Live Server extension, and localStorage data all stay intact.
Delete - I don't want to use this again
Remove all project files, the extension, and saved browser data.
- Stop Live Server if it is still running by clicking the Port: 5500 button in the VS Code status bar.
- Delete the project folder by running this command in your terminal:
macOS
rm -rf ~/Desktop/focus-flow
Windows
rmdir /s /q %USERPROFILE%\Desktop\focus-flow
- Clear your saved session data. Open the browser tab where your timer was running, press Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows) to open the browser console, and run these two commands:
localStorage.removeItem('focusFlowSessions');
localStorage.removeItem('focusFlowHistory');
- Uninstall the Live Server extension. In VS Code, press Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows) to open the Extensions view.
- Search for Live Server, click on it, then click Uninstall.
Nice Work!
Nice Work!
You just built a fully functional Pomodoro study timer from scratch. Every line of HTML, CSS, and JavaScript in Focus Flow is code you typed and understand.
You've learned how to:
- Built a professional dark-themed UI across three separate files (index.html, style.css, script.js), following the separation of concerns pattern that keeps structure, style, and behavior in their own files.
- Connected HTML elements to JavaScript using id attributes and getElementById, then made buttons respond to clicks with event listeners and built a countdown using setInterval.
- Used CSS custom properties to build a consistent dark theme with mode-aware color switching, and localStorage to persist session data across browser refreshes.
- Secret Mission: Added a session history log that combines DOM manipulation, localStorage, and dynamic HTML generation to display every completed focus session with its task name and timestamp.
Ready to quiz yourself?