Stopwatch
Home
Snippets
Stopwatch
HTML
CSS
JS
<div class="app"> <header> <h1>Stopwatch</h1> </header> <main> <section class="display-section" id="display" aria-live="polite"> <div class="display-deco"> <div class="time-group"> <span class="time minutes">00</span> <span class="label">MIN</span> </div> <span class="divider">:</span> <div class="time-group"> <span class="time seconds">00</span> <span class="label">SEC</span> </div> <span class="divider">.</span> <div class="time-group"> <span class="time milliseconds">00</span> <span class="label">MS</span> </div> </div> </section> <nav class="controls"> <button class="lap-btn" id="lap-btn" type="button" aria-label="Save laptime" disabled> Lap </button> <button class="toggle-btn" type="button" aria-pressed="false" aria-label="Start timer"> </button> <button class="reset-btn" type="button" aria-label="Reset timer" disabled> Reset </button> </nav> </main> <section class="laps-section" aria-labelledby="laps-title"> <h2 class="laps-title" id="laps-title">laptime</h2> <ol class="laps" aria-live="polite"></ol> </section> </div>
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); *, *::after, *::before { margin: 0; padding: 0; box-sizing: border-box; } :root { --bg-surface: #ffffff; --bg-surface-2: hsl(206 20% 96%); --bg-surface-3: hsl(206 14% 89%); --gray-0: hsl(196, 14%, 98%); --gray-1: hsl(195, 11%, 93%); --gray-2: hsl(198, 8%, 86%); --gray-3: hsl(196, 7%, 73%); --gray-4: hsl(198, 8%, 64%); --gray-5: hsl(196, 8%, 56%); --gray-6: hsl(198, 9%, 49%); --gray-7: hsl(200, 10%, 43%); --gray-8: hsl(202, 10%, 34%); --gray-9: hsl(200, 10%, 27%); --gray-10: hsl(196, 9%, 20%); --gray-11: hsl(193, 10%, 13%); --gray-12: hsl(187, 20%, 5%); --toggle-start: hsl(198 90 46); --toggle-pause: hsl(0 84 62); --add: hsl(148 69 67); --reset: hsl(210 12 97); } body { min-block-size: 100dvh; display: grid; place-content: start center; font-family: "Inter", sans-serif; position: relative; transform: scale(0.8); } .app { margin: 10vmin auto; position: fixed; inline-size: 360px; max-block-size: 680px; display: flex; flex-flow: column; background: linear-gradient( 135deg, var(--bg-surface-2) 19em, var(--bg-surface) 0 ); border: 1px solid var(--gray-2); border-radius: 1.5em; position: relative; box-shadow: 0 30px 60px -20px hsl(210 20% 20% / 0.4), 0 0 0 1px hsl(210 15% 94%), inset 0 1px 0 0 hsl(210 15% 100% / 0.8); } header { border-radius: 1.5em 1.5em 0 0; display: grid; height: 3.5rem; flex-shrink: 0; place-content: center; border-bottom: 1px solid var(--gray-3); background: var(--bg-surface); box-shadow: 0 3px 5px var(--gray-2); } h1 { font-weight: 600; font-size: 1rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--gray-8); } main { display: flex; flex-flow: column; gap: 1.5rem; } .display-section { display: grid; justify-content: center; margin: 1rem; padding-block-start: 0.5rem; } .display-deco { display: grid; grid-auto-flow: column; place-content: center; border-radius: 50%; min-height: 12.5rem; aspect-ratio: 1; border: 1px solid var(--gray-3); background: radial-gradient( 70% 70% at 30% 20%, var(--gray-0) 10% 40%, var(--gray-1), var(--gray-2) ); box-shadow: 0 0.75em 1.125em #0004; position: relative; } .display-deco::after, .display-deco::before { content: ""; position: absolute; border-radius: inherit; border: 0.25em solid; } .display-deco::after { inset: 0.375em; border-color: transparent var(--gray-7) var(--gray-7) transparent; } .display-deco::before { inset: -0.75em; border-color: var(--gray-2) transparent transparent var(--gray-2); } .time-group { display: grid; } .time { font-size: 2.25rem; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--gray-11); letter-spacing: -0.5px; } .divider { font-size: 1.875rem; font-weight: 600; margin-inline: 0.125rem; place-content: center; translate: 0 -0.25em; color: var(--gray-5); } .label { margin-top: 0.25rem; text-align: center; font-size: 0.75rem; letter-spacing: 0.15em; font-weight: 600; color: var(--gray-3); opacity: 0.8; } .controls { padding: 0.25rem 1rem; display: flex; gap: 0.75rem; justify-content: center; } [type="button"] { cursor: pointer; display: grid; place-content: center; width: 100%; block-size: 2.5rem; font-family: "Inter", sans-serif; font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; border-radius: 100vh; border: none; position: relative; outline: 1px solid transparent; } [type="button"]:not(:disabled) { color: var(--gray-12); background: linear-gradient(#fff6, #0000, #0002, #0004) var(--bg-color); } [type="button"]::after, [type="button"]::before { content: ""; position: absolute; inset: 0; border-radius: inherit; } [type="button"]::before { opacity: 1; box-shadow: 0 1.25px 0.5px 0.25px #0003, 0 3px 5px -1px #0003, 0 5px 9px -1px #0005; } [type="button"]::after { opacity: 0; box-shadow: inset 0 2px 3px 2px #0002, inset 0 1px 1px 0px #0006, 0 -2px 1px 0 #0002; } .toggle-btn[aria-pressed="false"] { --bg-color: var(--toggle-start); } .toggle-btn[aria-pressed="true"] { --bg-color: var(--toggle-pause); } .lap-btn { --bg-color: var(--add); } .reset-btn { --bg-color: var(--reset); } [type="button"]:active { scale: 0.97; background: var(--bg-color); } [type="button"]:disabled::before, [type="button"]:active::before { opacity: 0; } [type="button"]:active::after { opacity: 1; } [type="button"]:disabled { pointer-events: none; cursor: not-allowed; user-select: none; outline-color: var(--gray-2); background: var(--gray-1); } .laps-section { display: grid; overflow-y: auto; padding: 0.75rem 0.5rem; margin-block: 0.5rem; } .laps-title { width: max-content; margin-inline-start: 1rem; margin-block: 0.5rem 1rem; line-height: 1; font-weight: 600; font-size: 14px; text-transform: uppercase; letter-spacing: 0.13em; color: var(--gray-10); transition: opacity 230ms ease; } .laps-title.is-muted { opacity: 0.125; } .laps { list-style: none; display: grid; gap: 0.2rem; margin-inline: 0.5em; grid-auto-rows: 2.75rem; } .lap-item { display: flex; justify-content: space-between; align-items: center; padding-inline: 1rem; margin-inline: 0.5rem; border: 1px solid var(--gray-2); border-radius: 0.5em; font-size: 0.875rem; font-weight: 600; box-shadow: 0 3px 5px -2px hsl(190 7 32 / 0.5); background: var(--bg-surface); } .lap-label { color: var(--gray-8); } .lap-time { color: var(--toggle-start); font-variant-numeric: tabular-nums; } .is-new { animation: lapAnim 300ms cubic-bezier(0.14, 0.56, 0.15, 1.18) both; } @keyframes lapAnim { from { transform: scale(0); opacity: 0; } to { transform: scale(1); opacity: 1; } }
const stopwatch = () => { const toggleBtn = document.querySelector(".toggle-btn"); const resetBtn = document.querySelector(".reset-btn"); const lapBtn = document.querySelector(".lap-btn"); const lapsTitle = document.querySelector(".laps-title"); const [minutes, seconds, milliseconds] = [ ".minutes", ".seconds", ".milliseconds" ].map((sel) => document.querySelector(sel)); const lapsList = document.querySelector(".laps"); let rafId = null; let startTime = 0; let isRunning = false; const data = { elapsedTime: 0, laps: [] }; const formatElapsedTime = (ms) => { const pad = (n) => String(n).padStart(2, "0"); const total = Math.floor(ms); return { minutes: pad(Math.floor(total / 60000)), seconds: pad(Math.floor((total % 60000) / 1000)), milliseconds: pad(Math.floor((total % 1000) / 10)) }; }; const updateTimeDisplay = () => { const t = formatElapsedTime(data.elapsedTime); minutes.textContent = t.minutes; seconds.textContent = t.seconds; milliseconds.textContent = t.milliseconds; }; const updateElapsedTime = () => { if (!isRunning) return; data.elapsedTime = performance.now() - startTime; updateTimeDisplay(); rafId = requestAnimationFrame(updateElapsedTime); }; const updateLaptime = () => { const hasLaps = data.laps.length > 0; lapsTitle.classList.toggle("is-muted", !hasLaps); if (!hasLaps) { lapsList.replaceChildren(); return; } const time = data.laps.at(-1); const { minutes, seconds, milliseconds } = formatElapsedTime(time); const li = document.createElement("li"); li.className = "lap-item is-new"; li.innerHTML = ` <span class="lap-label">Lap ${data.laps.length}</span> <span class="lap-time">${minutes}:${seconds}.${milliseconds}</span> `; lapsList.append(li); }; const updateButtonStates = () => { toggleBtn.textContent = isRunning ? "Pause" : "Start"; toggleBtn.setAttribute("aria-pressed", String(isRunning)); lapBtn.disabled = !isRunning; resetBtn.disabled = isRunning || data.elapsedTime === 0; }; const toggleStopwatch = () => { isRunning = !isRunning; if (isRunning) { startTime = performance.now() - data.elapsedTime; rafId = requestAnimationFrame(updateElapsedTime); } else { cancelAnimationFrame(rafId); } updateButtonStates(); }; const addNewLap = () => { if (!isRunning) return; data.laps.push(data.elapsedTime); updateLaptime(); }; const reset = () => { if (isRunning) return; data.elapsedTime = 0; data.laps = []; updateTimeDisplay(); updateLaptime(); updateButtonStates(); }; toggleBtn.addEventListener("click", toggleStopwatch); lapBtn.addEventListener("click", addNewLap); resetBtn.addEventListener("click", reset); updateTimeDisplay(); updateLaptime(); updateButtonStates(); }; stopwatch();
Ad #1
Ad #2
Scroll to Top