HTML CSS JavaScript Project: Build a Study Timer (Real-Life Web Projects #1)
If you’re looking for a beginner-friendly HTML CSS JavaScript project, you’re in the right place! In this tutorial, we’ll build an html css javascript project study timer that helps you stay focused during study sessions. You’ll learn how to structure the page with HTML, style it with CSS, and bring it to life with JavaScript. By the end, you’ll have a practical project you can use every day — and a stronger foundation in web development.
This is part of our Real-Life Web Projects series, where we learn by building real apps you can actually use.
“Learn by building a real Pomodoro timer with HTML structure, CSS styling, and JavaScript logic – perfect for portfolio projects!”
Goal: Make a clean Pomodoro‑style timer that counts down, lets you pause/reset, set custom minutes, and plays a “ding” at the end.
Who it’s for: Absolute beginners to comfy beginners (teens & adults).
What you’ll practice: HTML structure → CSS styling → JavaScript timers & DOM updates.
What we’re building (quick preview)
- A big time display like
25:00
- Start / Pause / Reset buttons
- A box to set custom minutes (e.g., 10, 30, 60)
- A short sound when time hits zero
Live Preview of Study Timer Project
Files to create
Put these three files in one folder:
index.html
style.css
script.js
Open index.html
in your browser to run it.
Step 1: Write the HTML for your HTML CSS JavaScript Project Study Timer
Full code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Build a study timer with HTML, CSS, and JavaScript – beginner tutorial">
<title>Study Timer</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="container">
<h1>Study Timer ⏳</h1>
<div id="timer" aria-live="polite">25:00</div>
<div class="progress">
<div class="bar" id="bar"></div>
</div>
<div class="controls">
<button id="start">Start</button>
<button id="pause" disabled>Pause</button>
<button id="reset" disabled>Reset</button>
</div>
<div class="custom">
<label>
Custom Minutes:
<input type="number" id="minutes" min="1" max="180" value="25" />
</label>
<button id="apply">Apply</button>
</div>
<p id="error" class="error"></p>
<p class="tip">Pro tip: 25 min focus + 5 min break = Pomodoro method 🍅</p>
<audio id="ding" src="https://actions.google.com/sounds/v1/alarms/beep_short.ogg" preload="auto"></audio>
</main>
<script src="script.js"></script>
</body>
</html>
What each part means (friendly walkthrough)
<!DOCTYPE html>
— Enables modern HTML5 parsing rules.<html lang="en">
— Declares page language for accessibility and SEO.<meta charset="UTF-8">
— Supports Unicode (all typical characters + emoji).<meta name="viewport"...>
— Scales UI correctly on phones and tablets.<title>Study Timer</title>
— Sets the browser tab text.<link rel="stylesheet" href="style.css" />
— Loads your CSS file.<main class="container">
— Semantic wrapper for the app UI; also the centered card.<h1>Study Timer ⏳</h1>
— Clear, descriptive heading for readers and screen readers.<div id="timer">25:00</div>
— The big display the script updates every second.- Progress wrapper + fill:
<div class="progress">
— The track.<div class="bar" id="bar"></div>
— The fill you animate by changingwidth
in JS.
- Controls:
#start
— Starts the countdown.#pause
— Pauses (disabled initially to prevent “pause before start”).#reset
— Resets (disabled initially until a session starts).
- Custom minutes:
- Label +
<input type="number" id="minutes" min="1" max="180" value="25" />
— Browser enforces guard rails and numeric UI on mobile. <button id="apply">Apply</button>
— Applies the new duration.
<p id="error" class="error"></p>
— Space for friendly validation messages.<p class="tip">…</p>
— Helpful hint about Pomodoro (25 + 5).<audio id="ding" src="…beep_short.ogg" preload="auto"></audio>
— Short sound played at 0.<script src="script.js"></script>
— Loads logic after the DOM so element lookups work.
Optional a11y enhancement (no code change required): If you ever want screen readers to announce ticking changes, add
aria-live="polite"
on the timer div. It’s purely optional.
Step 2: Add CSS Styles for the Study Timer Project
Full code
:root {
--teal-start: #0EB4A5;
--teal-end: #00123A;
--bg: #0D1117;
--fg: #E2E8F0;
--muted: #94A3B8;
--accent: linear-gradient(135deg, var(--teal-start), #0A7C77 80%, var(--teal-end));
}
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.container {
width: min(560px, 92vw);
background: #0B1426;
border-radius: 16px;
padding: 28px;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, .35);
}
#timer {
font-size: 72px;
font-weight: 800;
letter-spacing: 2px;
margin: 16px 0;
}
.progress {
width: 100%;
height: 12px;
background: #1e293b;
border-radius: 999px;
overflow: hidden;
margin: 12px 0;
}
.bar {
height: 100%;
width: 0%;
background: var(--accent);
transition: width .2s linear;
}
.controls {
display: flex;
gap: 10px;
justify-content: center;
margin: 14px 0;
}
button {
border: none;
padding: 10px 16px;
border-radius: 10px;
background: #0A7C77;
color: var(--fg);
cursor: pointer;
}
button:hover {
transform: translateY(-1px);
opacity: .95;
}
#start {
background: var(--accent);
color: #06230f;
}
.error {
color: #f87171;
min-height: 1.2em;
}
.tip {
color: var(--muted);
margin-top: 12px;
}
Why this CSS works
Quick purpose: Dark, high-contrast UI with brand teal gradient; readable clock; smooth progress bar.
Theme tokens
:root { --teal-start, --teal-end, --bg, --fg, --muted, --accent }
Variables centralize colors so you can re-skin quickly.--accent
is a gradient (note the80%
stop in your code for a longer teal before it blends to--teal-end
).
Layout + container
body
uses flexbox to center.container
both ways; full-viewport height..container
is a card: fixed max width, rounded corners, shadow, and a dark panel background.
Clock + progress
#timer
is large (72px), bold, with extra letter spacing for clarity..progress
is the track;.bar
is the fill that JS widens from0%
to100%
..bar { transition: width .2s linear; }
gives you a smooth fill animation at each tick.
Controls + buttons
.controls
lays out buttons in a row with spacing.button:hover
adds a tiny lift for a “clickable” feel.#start
uses the accent gradient for emphasis. (You kept other buttons solid teal viabackground: #0A7C77;
— a nice contrast to the gradient Start.)
Messages
.error
styles validation messages in accessible red..tip
uses the muted color for friendly hints.
CSS Tricks Explained
- Why
:root
Variables?
“CSS variables (like--teal-start
) let you change colors globally in one place!” - Flexbox Centering:
“display: flex
+align-items: center
vertically centers the timer like magic!”
Safe tweak ideas (no JS changes): Adjust any
:root
color tokens to match future Codeboid palettes; the rest of the UI adapts automatically.
Step 3: JavaScript Logic for Your Study Timer Project
Full code
// ===== Study Timer (HTML/CSS/JS) — Codeboid edition =====
let total = 25 * 60; // total seconds
let remaining = total; // seconds left
let tick = null; // interval id
let running = false; // is the timer running?
// Elements
const elTime = document.getElementById('timer');
const elBar = document.getElementById('bar');
const elStart = document.getElementById('start');
const elPause = document.getElementById('pause');
const elReset = document.getElementById('reset');
const elMin = document.getElementById('minutes');
const elApply = document.getElementById('apply');
const elDing = document.getElementById('ding');
const elErr = document.getElementById('error');
// Helpers
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
const fmt = (s) => {
const m = Math.floor(s / 60).toString().padStart(2, '0');
const ss = (s % 60).toString().padStart(2, '0');
return `${m}:${ss}`;
};
const draw = () => {
elTime.textContent = fmt(remaining);
const pct = total ? (1 - remaining / total) * 100 : 0;
elBar.style.width = `${clamp(pct, 0, 100)}%`;
};
const setButtons = () => {
elStart.disabled = running;
elPause.disabled = !running;
elReset.disabled = !running && remaining === total;
};
// Core actions
function start() {
if (running) return;
running = true;
setButtons();
tick = setInterval(() => {
if (remaining > 0) {
remaining--;
draw();
} else {
stop();
// Friendly finish
try { elDing.play(); } catch { }
alert("Time's up! 🎉 Take a short break.");
}
}, 1000);
}
function stop() {
running = false;
clearInterval(tick);
tick = null;
setButtons();
}
function reset() {
stop();
remaining = total;
draw();
setButtons();
}
// Validate minutes input (1–180)
function applyMinutes() {
const raw = parseInt(elMin.value || '25', 10);
const mins = clamp(isNaN(raw) ? 25 : raw, 1, 180);
// Show friendly messages for out-of-range
if (raw < 1) elErr.textContent = 'Minimum is 1 minute.';
else if (raw > 180) elErr.textContent = 'That’s a long session! Please enter ≤ 180 minutes.';
else elErr.textContent = '';
total = mins * 60;
remaining = total;
draw();
setButtons();
// Persist a tiny preference (optional)
try { localStorage.setItem('cb:study:min', String(mins)); } catch { }
}
// Hook up events
elStart.addEventListener('click', start);
elPause.addEventListener('click', stop);
elReset.addEventListener('click', reset);
elApply.addEventListener('click', applyMinutes);
// Load saved minutes if present
(function init() {
try {
const saved = parseInt(localStorage.getItem('cb:study:min') || '25', 10);
if (!isNaN(saved)) elMin.value = clamp(saved, 1, 180);
} catch { }
applyMinutes(); // also draws initial 25:00
})();
What’s going on
Quick purpose: Manage session state, tick down once per second, update the UI, validate minutes, and play a sound at the end.
1) State
let total = 25 * 60;
— Total seconds for current session (defaults to 25 minutes).let remaining = total;
— Counts down to0
.let tick = null;
— Stores the interval ID so you can pause/clear it.let running = false;
— Prevents starting multiple intervals.
2) Element references
You grab everything once: timer, bar, buttons, input, audio, and error <p>
. This is efficient and keeps code readable.
3) Helpers
clamp(n, lo, hi)
— Keeps numbers inside a safe range (used for percent width + minutes).fmt(s)
— Converts seconds toMM:SS
with zero padding.draw()
— Updates:- The textual time:
elTime.textContent = fmt(remaining)
. - The progress bar width:
pct = (1 - remaining / total) * 100
, then clamp to 0–100%.
- The textual time:
setButtons()
— Enables/disables buttons:Start
is disabled while running.Pause
is enabled only while running.Reset
is disabled until the time has changed from its start value.
4) Core actions
start()
- Guard: if already running, ignore.
- Flip
running = true
, update buttons, and kick offsetInterval
every 1000ms. - Each tick:
- If
remaining > 0
, decrement anddraw()
. - Else,
stop()
, try to playding
, and show a friendly alert.
- If
stop()
- Flip
running = false
,clearInterval(tick)
, null it, and update buttons.
- Flip
reset()
- Call
stop()
(safety), setremaining = total
,draw()
, update buttons.
- Call
5) Validate & apply minutes
applyMinutes()
- Read
#minutes
, coerce to integer, clamp to1–180
. - Show friendly text in
#error
if out of range. - Set
total = mins * 60
andremaining = total
, thendraw()
and update buttons. - Save preference to
localStorage
(optional quality-of-life).
- Read
6) Events + init
- Click handlers:
start
,stop
,reset
,applyMinutes
. - On load, try to read a saved minutes value, clamp it, then call
applyMinutes()
to initialize the UI and drawMM:SS
.
Concept Breakdown:
// setInterval vs setTimeout
// - setInterval: Repeats every X ms (perfect for timers!)
// - setTimeout: Runs once after X ms
Debugging Tip:
“Stuck? Add console.log(timeLeft)
to check timer values!”
Why
setInterval
? It’s straightforward and perfect for a study timer. If you ever need rock-solid background accuracy, you could compute elapsed time fromDate.now()
each frame—but your current approach is ideal for beginners and typical use.
How to Use (User-Facing)
- All three files are in the same folder.
- Open
index.html
directly in your browser. - Set minutes (or keep 25).
- Click Start — watch the clock and progress bar.
- Click Pause to stop the tick; Start resumes.
- Click Reset to return to the session’s start time.
- When time hits zero, you’ll hear a quick ding and see an alert.
Test Checklist
- Start → numbers count down once per second.
- Pause → time stops changing; Start re-enables, Pause disables.
- Reset → time snaps back to the start value; Reset disables if nothing has progressed yet.
- Apply → entering 10 shows
10:00
and resets the bar to0%
. - Time’s up → you hear the ding (after at least one interaction if your browser requires it) and see an alert.
Troubleshooting (Quick Fixes)
- Buttons do nothing → Make sure your HTML IDs exactly match the ones used in JS.
- Timer goes negative → Ensure
stop()
is called whenremaining
hits0
. (It is, inside your code.) - No sound → Some browsers block autoplay until a click. Your
try { elDing.play(); } catch {}
avoids errors; one interaction unlocks audio. - Progress bar isn’t moving → Confirm
id="bar"
in HTML and.bar { transition: width .2s; }
in CSS. - Disabled buttons look the same → Browsers don’t always dim disabled buttons by default. (You’re functionally correct; styling is optional and doesn’t affect behavior.)
FAQ
Why the 1–180 minute range?
It’s a practical ceiling to prevent typos (e.g., 9999 minutes) and fit common study blocks.
Can I use it on mobile?
Yes. The layout is responsive and the number input uses a numeric keypad.
Will it keep running in a background tab?
Yes, though background throttling may slow the visual updates slightly. For a study timer, this is fine.
How do I change the default 25 minutes?
Change the 25
in let total = 25 * 60;
near the top of your JS.
Try It Yourself
- Easy: Change the “ding” sound
- Medium: Add a break timer (5 minutes after each session)
- Hard: Save session history using
localStorage
Codeboid Series Wrap-Up + Next Steps
You just built a real, useful app with HTML structure, clean dark UI, and JavaScript timers + DOM updates—very much the Codeboid way: learn by doing.
Next in Real-Life Web Projects: Recipe Finder (API + search) — practice fetching JSON, rendering cards, and adding a search filter.