Stopwatch App with HTML, CSS & JavaScript
20 DAYS 20 PROJECT CHALLENGE
Day #04
Project Overview
A compact Stopwatch built with HTML, CSS and JavaScript. It demonstrates setInterval() and clearInterval() for timing, plus basic time arithmetic to convert milliseconds into minutes, seconds, and hundredths. The UI provides Start, Stop, Reset (and optional Lap) controls, a large time display, and a simple lap list. This is a beginner-friendly project to learn timing logic, DOM updates, and event handling.
Key Features
- Start / Stop / Reset controls.
- Optionally record laps (click Lap to save current time).
- Smooth updates using
setInterval()(10–100 ms tick). - Accurate time accumulation using elapsed milliseconds (works when pausing/resuming).
- Responsive, keyboard-accessible controls and basic ARIA attributes.
- Minimal, easy-to-read code you can extend (split/split-lap, save to localStorage, export).
HTML Code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Day 4 — Stopwatch</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="card" role="main" aria-labelledby="title">
<header>
<div class="logo">SW</div>
<div>
<h1 id="title">Day 4: Stopwatch</h1>
<p class="lead">A simple stopwatch using <code>setInterval()</code>, <code>clearInterval()</code>, and time arithmetic.</p>
</div>
</header>
<section class="stopwatch">
<div class="display" id="display" aria-live="polite">00:00:00</div>
<div class="controls" role="group" aria-label="Stopwatch controls">
<button id="startBtn" class="btn">Start</button>
<button id="stopBtn" class="btn secondary" disabled>Stop</button>
<button id="lapBtn" class="btn secondary" disabled>Lap</button>
<button id="resetBtn" class="btn secondary" disabled>Reset</button>
</div>
<section class="laps" aria-label="Laps">
<h2 class="small">Laps</h2>
<ol id="lapsList" class="laps-list" aria-live="polite"></ol>
</section>
<details style="margin-top:12px">
<summary>How it works (short)</summary>
<p>The script keeps an elapsed milliseconds counter. On Start it notes the start timestamp and uses <code>setInterval()</code> to update the display frequently. On Stop it clears the interval and accumulates elapsed time. Reset clears everything.</p>
</details>
</section>
</main>
<script src="script.js"></script>
</body>
</html>
CSS Code
:root {
--bg: #0f1724;
--card: #071027;
--accent: #7c3aed;
--muted: #9aa4b2;
--glass: rgba(255, 255, 255, 0.03);
font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, Arial;
}
html,
body {
height: 100%;
}
body {
margin: 0;
background: #002252;
color: #e6eef6;
display: flex;
align-items: center;
justify-content: center;
padding: 28px;
}
.card {
width: min(720px, 96%);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
border-radius: 12px;
padding: 20px;
box-shadow: 0 8px 30px rgba(2, 6, 23, 0.6);
border: 1px solid rgba(255, 255, 255, 0.03);
}
header {
display: flex;
gap: 14px;
align-items: center;
}
.logo {
width: 44px;
height: 44px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent), #22c1c3);
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
}
h1 {
margin: 0;
font-size: 18px;
}
.lead {
margin: 4px 0 12px;
color: var(--muted);
font-size: 14px;
}
.stopwatch {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 6px;
}
.display {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, 'Roboto Mono', monospace;
font-size: 48px;
text-align: center;
padding: 18px 12px;
border-radius: 10px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.015), rgba(255, 255, 255, 0.01));
border: 1px solid rgba(255, 255, 255, 0.03);
}
.controls {
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 10px 14px;
border-radius: 10px;
border: 0;
background: linear-gradient(90deg, var(--accent), #22c1c3);
color: white;
font-weight: 600;
cursor: pointer
}
.btn.secondary {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.06);
color: var(--muted)
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed
}
.small {
font-size: 13px;
color: var(--muted);
margin: 0;
}
.laps {
margin-top: 6px;
}
.laps-list {
padding-left: 18px;
margin: 6px 0 0;
max-height: 160px;
overflow: auto;
}
.laps-list li {
padding: 6px 8px;
border-radius: 8px;
margin-bottom: 6px;
background: rgba(255, 255, 255, 0.02);
font-family: ui-monospace, monospace;
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.laps-list li span:nth-child(1) {
color: var(--muted);
font-size: 13px
}
.laps-list li span:nth-child(2) {
font-weight: 600
}
@media (max-width:480px) {
.display {
font-size: 36px;
padding: 14px;
}
.controls {
gap: 8px;
}
.btn {
padding: 8px 10px;
}
} Javascript Code
// Stopwatch logic using elapsed ms and setInterval
const display = document.getElementById('display');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const resetBtn = document.getElementById('resetBtn');
const lapBtn = document.getElementById('lapBtn');
const lapsList = document.getElementById('lapsList');
let intervalId = null; // reference to setInterval
let startTimestamp = 0; // when the current run started (performance.now())
let elapsed = 0; // accumulated elapsed ms across runs
const tickMs = 31; // update every ~31ms (about 30fps). Use 10 for 100Hz.
function formatTime(ms){
const totalHundredths = Math.floor(ms / 10); // hundredths of second
const hundredths = totalHundredths % 100;
const totalSeconds = Math.floor(ms / 1000);
const seconds = totalSeconds % 60;
const minutes = Math.floor(totalSeconds / 60) % 60;
const hours = Math.floor(totalSeconds / 3600);
// Format as HH:MM:SS:hh (we'll show HH:MM:SS if hours=0)
const two = v => String(v).padStart(2,'0');
if(hours > 0){
return `${two(hours)}:${two(minutes)}:${two(seconds)}`;
}else{
return `${two(minutes)}:${two(seconds)}:${two(hundredths)}`;
}
}
function updateDisplay(){
const now = performance.now();
const currentElapsed = elapsed + (startTimestamp ? (now - startTimestamp) : 0);
display.textContent = formatTime(Math.floor(currentElapsed));
}
function start(){
if(intervalId) return; // already running
startTimestamp = performance.now();
intervalId = setInterval(updateDisplay, tickMs);
// update UI
startBtn.disabled = true;
stopBtn.disabled = false;
resetBtn.disabled = false;
lapBtn.disabled = false;
}
function stop(){
if(!intervalId) return;
clearInterval(intervalId);
intervalId = null;
// accumulate elapsed
const now = performance.now();
elapsed += (now - startTimestamp);
startTimestamp = 0;
updateDisplay();
// update UI
startBtn.disabled = false;
stopBtn.disabled = true;
lapBtn.disabled = true;
}
function reset(){
// stop first
if(intervalId) clearInterval(intervalId);
intervalId = null;
startTimestamp = 0;
elapsed = 0;
display.textContent = '00:00:00';
// clear laps
lapsList.innerHTML = '';
// update UI
startBtn.disabled = false;
stopBtn.disabled = true;
resetBtn.disabled = true;
lapBtn.disabled = true;
}
function lap(){
// record current elapsed time (do nothing if not started)
const now = performance.now();
const currentElapsed = elapsed + (startTimestamp ? (now - startTimestamp) : 0);
const li = document.createElement('li');
const idx = lapsList.children.length + 1;
const left = document.createElement('span');
left.textContent = `Lap ${idx}`;
const right = document.createElement('span');
right.textContent = formatTime(Math.floor(currentElapsed));
li.appendChild(left);
li.appendChild(right);
// prepend newest on top
lapsList.insertBefore(li, lapsList.firstChild);
// enable reset if not already
resetBtn.disabled = false;
}
// hook up events
startBtn.addEventListener('click', start);
stopBtn.addEventListener('click', stop);
resetBtn.addEventListener('click', reset);
lapBtn.addEventListener('click', lap);
// keyboard shortcuts: Space to start/stop, L to lap, R to reset
document.addEventListener('keydown', (e) => {
if(e.key === ' '){ // space start/stop
e.preventDefault();
if(intervalId) stop(); else start();
} else if(e.key.toLowerCase() === 'l'){
if(!lapBtn.disabled) lap();
} else if(e.key.toLowerCase() === 'r'){
reset();
}
});
// initialize
reset(); // sets initial UI state
Related Projects
Day 2 : Password Generator
Generates random secure passwords with adjustable length and character types.
Concepts: Arrays, string manipulation, clipboard API.
Day 6 : Tip Calculator
Calculates tip amount and total per person based on bill and tip %.
Concepts: Form inputs, math logic.
Day 7 : Expense Tracker
Track income and expenses, and calculate the total balance.
Concepts: LocalStorage, array methods (map, reduce).