Stopwatch App with HTML, CSS & JavaScript
20 DAYS 20 PROJECT CHALLENGE
Day #04
Project Overview
The Stopwatch App is a beginner-friendly project created with HTML, CSS, and JavaScript. It demonstrates a stopwatch with start, stop, reset, and lap options that users can apply to the running timer.
The application uses the setInterval() and clearInterval() methods of JavaScript to allow users to measure time. In addition, the developed stopwatch has the capability to record the millisecond value and update it on the page. For this purpose, the code uses setInterval() and clearInterval() to calculate the current time in minutes, seconds, and hundredths of a second.
The developed application also allows users to use keyboard shortcuts and have a list of their laps on the screen. Thus, the Stopwatch app is a great example of learning how to work with time and create interactive web elements with HTML and CSS.
Key Features
- Start, Stop, and Reset Buttons: Users can start the stopwatch, pause it when needed, and reset the timer back to 00:00:00. The buttons also change their enabled and disabled states depending on the current stopwatch state.
- Lap Recordings: The Lap button records the current elapsed time and adds it to the lap list. The new lap recordings appear at the top so that users can easily see the latest added time.
- Elaborate Time Counting: This app calculates the elapsed time using milliseconds instead of using the setInterval() function. Thus, the stopwatch can keep the accumulated time when users stop and start the stopwatch.
- Smooth Time Updating: The stopwatch changes the displayed time every 31 milliseconds using the setInterval() function. This way, the time updates are smooth, and the implementation is easy to understand for beginners.
- Time Conversion and Display: JavaScript converts milliseconds to minutes, seconds, and hundredths of a second. When the stopwatch reaches an hour, it displays the time in hours.
- Keyboard Shortcuts: Users can control the stopwatch using their keyboards. The Space key starts and stops the timer, and the L and R keys record laps and reset the stopwatch, respectively.
- Responsive Design: The layout adjusts for smaller screens using a media query. The display becomes smaller and the control buttons use more compact spacing on mobile devices.
- Accessibility: The project uses aria-label and aria-live attributes to be more accessible and provide useful information for screen readers. It also uses the disabled attribute for buttons to indicate the current state.
- Easy Customization: The JavaScript code is open for extension and allows users to, for example, save their laps in the browser using the localStorage API or export the recorded laps as a.csv file or add split-time recording functionality.
What You’ll Learn
- Understand how HTML creates the structure of a stopwatch.
- Learn how
setInterval()repeatedly runs a function. - Explore how
clearInterval()stops a running timer. - Practice working with elapsed time in milliseconds.
- Understand how JavaScript converts milliseconds into readable time.
- Learn how DOM elements can update dynamically.
- Practice creating and inserting new HTML elements with JavaScript.
- Explore how event listeners respond to button clicks.
- Learn how keyboard events can control an application.
- Understand how HTML, CSS, and JavaScript work together.
HTML Code
The HTML code creates the main structure of the Stopwatch App. The <main> element uses the .card class as the primary container, while the header contains the project title and a short description. The .display element has the id="display" and initially shows 00:00:00. JavaScript updates this element whenever the stopwatch runs.
The .controls section contains four buttons: #startBtn, #stopBtn, #lapBtn, and #resetBtn. Each button performs a different stopwatch action. The Stop, Lap, and Reset buttons start as disabled because the stopwatch has not started yet. Additionally, the .laps-list element uses id="lapsList" to provide a location where JavaScript can dynamically add recorded lap times. Finally, the HTML file loads script.js at the bottom of the <body>.
<!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
The CSS code creates the dark, compact design of the Stopwatch App. The :root section defines reusable variables such as the background, card, accent, muted text, and glass-effect colors. The .card class controls the main container’s width, background, padding, rounded corners, border, and shadow. Next, the .stopwatch class uses Flexbox to arrange the display, controls, and lap section vertically.
The .display class gives the timer a large monospace font and centers the time on the page. The .controls class uses Flexbox to arrange the buttons while allowing them to wrap when the available space becomes smaller. In addition, .laps-list li styles each recorded lap and uses Flexbox to place the lap number and time apart. Finally, the @media (max-width:480px) rule reduces the display size and button padding on smaller screens.
: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
The JavaScript code controls the complete stopwatch functionality. First, the code selects the display, Start, Stop, Reset, Lap buttons, and lap list using getElementById(). The variables intervalId, startTimestamp, and elapsed keep track of the timer state. The formatTime() function converts milliseconds into hours, minutes, seconds, and hundredths. Next, updateDisplay() calculates the current elapsed time and updates the #display element.
The start() function records the starting timestamp and begins setInterval(). The stop() function uses clearInterval() and adds the current run time to elapsed. After that, reset() clears the timer and removes all lap entries. The lap() function creates new <li> elements and adds the current time to the lap list. Finally, event listeners connect the buttons and keyboard shortcuts to the corresponding functions.
// 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).