Source Code

<div class="lsw-wrap">
  <div class="lsw-display" id="lswDisplay">00:00.00</div>

  <div class="lsw-controls">
    <button class="lsw-btn lsw-btn-primary" id="lswStartBtn">Start</button>
    <button class="lsw-btn" id="lswLapBtn" disabled>Lap</button>
    <button class="lsw-btn" id="lswResetBtn">Reset</button>
  </div>

  <div class="lsw-laps" id="lswLaps"></div>
</div>

Lap Stopwatch with Splits — Free HTML CSS JS Snippet

Lap Stopwatch with Splits · Misc · Plain HTML, CSS & JS · Live preview

What's included

Features

Elapsed time computed from real Date.now() timestamps rather than a naively incremented counter
requestAnimationFrame-driven display updates decoupled from the underlying timing calculation
Accurate accumulation across multiple start/stop cycles with no drift or lost time
Lap recording that stores total elapsed time, with per-lap split times derived automatically
Most recent lap shown at the top of the list for easy at-a-glance tracking during use
MM: SS.ms display with centisecond precision and tabular-numeral formatting to avoid layout jitter
Lap button disabled while stopped, preventing laps from being recorded outside an active run
Full reset clears both the elapsed time and the recorded lap history

About this UI Snippet

Lap Stopwatch with Splits — Timestamp-Based Stopwatch and Lap Tracker

Screenshot of the Lap Stopwatch with Splits snippet rendered live

A stopwatch that simply adds a fixed amount to a counter on every tick will lose accuracy, because timers in the browser aren't perfectly regular — especially across pause/resume cycles. This snippet instead always derives elapsed time from real timestamps, keeping it accurate no matter how the render loop is actually scheduled.

Elapsed time derived from Date.now(), not accumulated per-tick

When the stopwatch starts, startTimestamp = Date.now() is captured once. currentElapsed() then always computes elapsed time as accumulatedMs + (Date.now() - startTimestamp) while running — the accumulated time from any previous run-then-stop segments, plus how long the current segment has been running, both measured against real timestamps rather than an incrementing counter.

requestAnimationFrame drives the display, not the timing

A requestAnimationFrame loop calls renderDisplay() on every frame while running, but the loop's only job is to repaint the display as often as the browser can manage — the actual elapsed-time value it renders always comes fresh from currentElapsed()'s timestamp math, so frame-rate variance never affects timing accuracy, only how smoothly the display updates.

Stop preserves exact accumulated time across sessions

Clicking Stop adds Date.now() - startTimestamp (the just-finished segment's real duration) into accumulatedMs and cancels the animation frame loop. Starting again later captures a new startTimestamp and resumes counting from the previously accumulated total — so multiple start/stop cycles never lose or double-count time.

Laps store total elapsed time; splits are derived

Clicking Lap pushes the current total elapsed time (not a duration) into the laps array. renderLaps() then computes each lap's split by subtracting the previous lap's total from the current one, displaying both the split and the running total for every lap, with the most recent lap listed first.

Step by step

How to Use

  1. 1
    Load the snippetClick "Lap Stopwatch with Splits" in the sidebar to load its HTML, CSS, and JS into the editor panels. The preview updates instantly.
  2. 2
    Edit the codeModify any panel — HTML, CSS, or JS. The preview refreshes as you type. Use Reset in each panel header to restore the original.
  3. 3
    Preview on devicesClick the Mobile (375px), Tablet (768px), or Desktop buttons in the preview header to check responsiveness.
  4. 4
    Export in your formatClick "HTML" to download a standalone file, "JSX" for a React component, "Tailwind" for a React + Tailwind CSS component, "Tailwind HTML" for a standalone HTML file with Tailwind CDN, "Vue" for a Vue 3 SFC with <template>/<script setup>/<style scoped>, or "Angular" for a standalone Angular .component.ts file. "Copy all" copies the full code to clipboard.
  5. 5
    Save your versionClick "Save as", type a name, and press Enter. Your snippet saves to IndexedDB and appears in the Saved tab.

Real-world uses

Common Use Cases

Fitness, running, and workout tracking apps
Track interval splits during a workout with an accurate, drift-free stopwatch.
Timed testing, cooking, or productivity tools
Any interface needing precise elapsed-time tracking with checkpoint recording.
Teaching timestamp-based timing over naive counters
A clear demonstration of why real timestamps beat incrementing a counter each tick for accuracy.
Dashboard widgets needing a precise running clock
A self-contained stopwatch component with lap history for any monitoring or session UI.

Got questions?

Frequently Asked Questions

setInterval and requestAnimationFrame callbacks don't fire at perfectly regular intervals — small delays accumulate over time. By deriving elapsed time as accumulatedMs + (Date.now() - startTimestamp), the displayed time is always mathematically correct regardless of any variance in how often the render loop actually runs.

It records the current total elapsed time (via currentElapsed()) into the laps array. The split time shown for each lap is then computed by subtracting the previous lap's total from the current one, not stored directly.

No — stopping adds the just-completed segment's duration into accumulatedMs, and restarting begins a new segment from a fresh startTimestamp, so total elapsed time across any number of stop/start cycles stays exactly accurate.

requestAnimationFrame syncs the display refresh to the browser's paint cycle, giving a smooth-looking update without wasting work when the tab isn't visible, while the actual timing math stays independent of the loop's exact frequency.