UI Components Easy
Stopwatch
A precision stopwatch with lap tracking, start/split/reset controls, and smooth animations.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--sw-bg: #1e1b4b;
--sw-text: #e0e7ff;
--sw-accent-primary: #818cf8;
--sw-accent-success: #34d399;
--sw-accent-danger: #f87171;
--sw-card: rgba(49, 46, 129, 0.5);
--body-bg: #1e1b4b;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
background: var(--body-bg);
color: var(--sw-text);
font-family: "Inter", system-ui, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
.stopwatch-widget {
width: 100%;
max-width: 400px;
background: rgba(255, 255, 255, 0.03);
padding: clamp(1.25rem, 4vw, 2.5rem);
border-radius: 32px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.stopwatch-display {
font-family: "JetBrains Mono", "Fira Code", monospace;
font-size: clamp(2.5rem, 10vw, 3.5rem);
text-align: center;
padding: 1.5rem 0;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--sw-accent-primary);
text-shadow: 0 0 20px rgba(129, 140, 248, 0.3);
line-height: 1;
}
.ms {
font-size: clamp(1rem, 4vw, 1.5rem);
opacity: 0.6;
}
.sw-controls {
display: flex;
justify-content: center;
gap: 0.75rem;
margin-bottom: 2rem;
}
.sw-btn {
padding: 0.75rem 0.5rem;
border-radius: 12px;
border: none;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
flex: 1;
font-size: clamp(0.75rem, 2.5vw, 0.875rem);
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
}
.sw-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.sw-btn.start {
background: var(--sw-accent-success);
color: #064e3b;
}
.sw-btn.stop {
background: var(--sw-accent-danger);
color: #7f1d1d;
}
.sw-btn.lap {
background: var(--sw-card);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.sw-btn.reset {
background: transparent;
color: var(--sw-text);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.sw-laps-container {
max-height: 180px;
overflow-y: auto;
padding-right: 0.5rem;
margin-top: 1rem;
}
.sw-laps-container::-webkit-scrollbar {
width: 4px;
}
.sw-laps-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
.sw-laps-list {
list-style: none;
padding: 0;
margin: 0;
}
.sw-laps-list li {
display: flex;
justify-content: space-between;
padding: 0.75rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
font-size: 0.813rem;
font-family: inherit;
}
.sw-laps-list li:first-child {
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.lap-num {
color: var(--sw-accent-primary);
opacity: 0.8;
font-weight: 700;
}
@media (max-width: 360px) {
.sw-controls {
gap: 0.5rem;
}
}let swStartTime;
let swElapsedTime = 0;
let swTimerInterval;
let swLaps = [];
const swMinutesEl = document.getElementById("sw-minutes");
const swSecondsEl = document.getElementById("sw-seconds");
const swMsEl = document.getElementById("sw-ms");
const swStartBtn = document.getElementById("sw-start");
const swLapBtn = document.getElementById("sw-lap");
const swResetBtn = document.getElementById("sw-reset");
const swLapsList = document.getElementById("sw-laps-list");
function timeToString(time) {
let diffInMin = time / (1000 * 60);
let mm = Math.floor(diffInMin);
let diffInSec = (diffInMin - mm) * 60;
let ss = Math.floor(diffInSec);
let diffInMs = (diffInSec - ss) * 1000;
let ms = Math.floor(diffInMs);
let formattedMM = mm.toString().padStart(2, "0");
let formattedSS = ss.toString().padStart(2, "0");
let formattedMS = ms.toString().padStart(3, "0");
return `${formattedMM}:${formattedSS}.${formattedMS}`;
}
function print(txt) {
const parts = txt.split(/[:\.]/);
if (swMinutesEl) swMinutesEl.innerHTML = parts[0];
if (swSecondsEl) swSecondsEl.innerHTML = parts[1];
if (swMsEl) swMsEl.innerHTML = parts[2];
}
function startStopwatch() {
swStartTime = Date.now() - swElapsedTime;
swTimerInterval = setInterval(function printTime() {
swElapsedTime = Date.now() - swStartTime;
print(timeToString(swElapsedTime));
}, 10);
showButton("STOP");
swLapBtn.disabled = false;
}
function stopStopwatch() {
clearInterval(swTimerInterval);
showButton("START");
}
function resetStopwatch() {
clearInterval(swTimerInterval);
print("00:00.000");
swElapsedTime = 0;
swLaps = [];
swLapsList.innerHTML = "";
showButton("START");
swLapBtn.disabled = true;
}
function recordLap() {
const lapTime = timeToString(swElapsedTime);
swLaps.unshift(lapTime);
const li = document.createElement("li");
li.innerHTML = `
<span class="lap-num">Lap ${swLaps.length}</span>
<span class="lap-time">${lapTime}</span>
`;
swLapsList.prepend(li);
}
function showButton(buttonKey) {
if (buttonKey === "STOP") {
swStartBtn.innerHTML = "Stop";
swStartBtn.className = "sw-btn stop";
} else {
swStartBtn.innerHTML = "Start";
swStartBtn.className = "sw-btn start";
}
}
// Event Listeners
if (swStartBtn) {
swStartBtn.addEventListener("click", function () {
if (swStartBtn.innerHTML === "Start") {
startStopwatch();
} else {
stopStopwatch();
}
});
}
if (swLapBtn) {
swLapBtn.addEventListener("click", recordLap);
}
if (swResetBtn) {
swResetBtn.addEventListener("click", resetStopwatch);
}<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stopwatch</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="stopwatch-widget">
<div class="stopwatch-display">
<span id="sw-minutes">00</span>:<span id="sw-seconds">00</span>.<span id="sw-ms" class="ms">000</span>
</div>
<div class="sw-controls">
<button id="sw-start" class="sw-btn start">Start</button>
<button id="sw-lap" class="sw-btn lap" disabled>Lap</button>
<button id="sw-reset" class="sw-btn reset">Reset</button>
</div>
<div class="sw-laps-container">
<ul id="sw-laps-list" class="sw-laps-list">
<!-- Laps will appear here -->
</ul>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useEffect, useRef } from "react";
function format(ms: number) {
const cents = Math.floor((ms % 1000) / 10);
const secs = Math.floor(ms / 1000) % 60;
const mins = Math.floor(ms / 60000) % 60;
const hrs = Math.floor(ms / 3600000);
return `${String(hrs).padStart(2, "0")}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}.${String(cents).padStart(2, "0")}`;
}
export default function StopwatchRC() {
const [elapsed, setElapsed] = useState(0);
const [running, setRunning] = useState(false);
const [laps, setLaps] = useState<number[]>([]);
const startRef = useRef<number>(0);
const baseRef = useRef<number>(0);
useEffect(() => {
if (!running) return;
startRef.current = Date.now();
const id = setInterval(() => {
setElapsed(baseRef.current + Date.now() - startRef.current);
}, 10);
return () => clearInterval(id);
}, [running]);
function toggle() {
if (running) {
baseRef.current = elapsed;
}
setRunning((r) => !r);
}
function reset() {
setRunning(false);
setElapsed(0);
baseRef.current = 0;
setLaps([]);
}
function lap() {
if (running) setLaps((prev) => [elapsed, ...prev]);
}
return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center p-6">
<div className="w-full max-w-sm">
<div className="bg-[#161b22] border border-[#30363d] rounded-2xl p-8 text-center mb-4">
<p className="font-mono text-[52px] font-bold text-[#e6edf3] tabular-nums leading-none mb-6">
{format(elapsed)}
</p>
<div className="flex gap-3 justify-center">
<button
onClick={toggle}
className={`flex-1 py-2.5 rounded-lg font-semibold text-sm transition-colors ${
running
? "bg-[#f85149]/10 border border-[#f85149]/30 text-[#f85149] hover:bg-[#f85149]/20"
: "bg-[#238636] border border-[#2ea043] text-white hover:bg-[#2ea043]"
}`}
>
{running ? "Stop" : "Start"}
</button>
<button
onClick={lap}
disabled={!running}
className="flex-1 py-2.5 rounded-lg font-semibold text-sm bg-[#21262d] border border-[#30363d] text-[#8b949e] hover:text-[#e6edf3] disabled:opacity-40 transition-colors"
>
Lap
</button>
<button
onClick={reset}
className="flex-1 py-2.5 rounded-lg font-semibold text-sm bg-[#21262d] border border-[#30363d] text-[#8b949e] hover:text-[#e6edf3] transition-colors"
>
Reset
</button>
</div>
</div>
{laps.length > 0 && (
<div className="bg-[#161b22] border border-[#30363d] rounded-xl overflow-hidden">
<div className="max-h-48 overflow-y-auto divide-y divide-[#21262d]">
{laps.map((lapTime, i) => (
<div key={i} className="flex justify-between px-4 py-2.5 text-sm">
<span className="text-[#8b949e]">Lap {laps.length - i}</span>
<span className="font-mono text-[#e6edf3] tabular-nums">{format(lapTime)}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}<script setup>
import { ref, onUnmounted } from "vue";
const elapsed = ref(0);
const running = ref(false);
const laps = ref([]);
let startTime = 0;
let base = 0;
let interval;
function format(ms) {
const cents = Math.floor((ms % 1000) / 10);
const secs = Math.floor(ms / 1000) % 60;
const mins = Math.floor(ms / 60000) % 60;
const hrs = Math.floor(ms / 3600000);
return `${String(hrs).padStart(2, "0")}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}.${String(cents).padStart(2, "0")}`;
}
function toggle() {
if (running.value) {
base = elapsed.value;
clearInterval(interval);
running.value = false;
} else {
startTime = Date.now();
running.value = true;
interval = setInterval(() => {
elapsed.value = base + Date.now() - startTime;
}, 10);
}
}
function reset() {
clearInterval(interval);
running.value = false;
elapsed.value = 0;
base = 0;
laps.value = [];
}
function lap() {
if (running.value) {
laps.value = [elapsed.value, ...laps.value];
}
}
onUnmounted(() => {
clearInterval(interval);
});
</script>
<template>
<div class="page">
<div class="wrapper">
<div class="card">
<p class="display">{{ format(elapsed) }}</p>
<div class="buttons">
<button
:class="['btn', running ? 'stop' : 'start']"
@click="toggle"
>
{{ running ? "Stop" : "Start" }}
</button>
<button class="btn secondary" :disabled="!running" @click="lap">Lap</button>
<button class="btn secondary" @click="reset">Reset</button>
</div>
</div>
<div v-if="laps.length > 0" class="laps">
<div class="laps-scroll">
<div v-for="(lapTime, i) in laps" :key="i" class="lap-row">
<span class="lap-label">Lap {{ laps.length - i }}</span>
<span class="lap-time">{{ format(lapTime) }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.page {
min-height: 100vh;
background: #0d1117;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.wrapper {
width: 100%;
max-width: 384px;
}
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 1rem;
padding: 2rem;
text-align: center;
margin-bottom: 1rem;
}
.display {
font-family: monospace;
font-size: 52px;
font-weight: 700;
color: #e6edf3;
font-variant-numeric: tabular-nums;
line-height: 1;
margin: 0 0 1.5rem;
}
.buttons {
display: flex;
gap: 0.75rem;
justify-content: center;
}
.btn {
flex: 1;
padding: 0.625rem 0;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn.start {
background: #238636;
border: 1px solid #2ea043;
color: white;
}
.btn.start:hover { background: #2ea043; }
.btn.stop {
background: rgba(248, 81, 73, 0.1);
border: 1px solid rgba(248, 81, 73, 0.3);
color: #f85149;
}
.btn.stop:hover { background: rgba(248, 81, 73, 0.2); }
.btn.secondary {
background: #21262d;
border: 1px solid #30363d;
color: #8b949e;
}
.btn.secondary:hover { color: #e6edf3; }
.btn.secondary:disabled { opacity: 0.4; cursor: not-allowed; }
.laps {
background: #161b22;
border: 1px solid #30363d;
border-radius: 0.75rem;
overflow: hidden;
}
.laps-scroll {
max-height: 192px;
overflow-y: auto;
}
.lap-row {
display: flex;
justify-content: space-between;
padding: 0.625rem 1rem;
font-size: 0.875rem;
border-bottom: 1px solid #21262d;
}
.lap-row:last-child { border-bottom: none; }
.lap-label { color: #8b949e; }
.lap-time {
font-family: monospace;
color: #e6edf3;
font-variant-numeric: tabular-nums;
}
</style><script>
import { onDestroy } from "svelte";
let elapsed = 0;
let running = false;
let laps = [];
let startTime = 0;
let base = 0;
let interval;
function format(ms) {
const cents = Math.floor((ms % 1000) / 10);
const secs = Math.floor(ms / 1000) % 60;
const mins = Math.floor(ms / 60000) % 60;
const hrs = Math.floor(ms / 3600000);
return `${String(hrs).padStart(2, "0")}:${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}.${String(cents).padStart(2, "0")}`;
}
function toggle() {
if (running) {
base = elapsed;
clearInterval(interval);
running = false;
} else {
startTime = Date.now();
running = true;
interval = setInterval(() => {
elapsed = base + Date.now() - startTime;
}, 10);
}
}
function reset() {
clearInterval(interval);
running = false;
elapsed = 0;
base = 0;
laps = [];
}
function lap() {
if (running) {
laps = [elapsed, ...laps];
}
}
onDestroy(() => {
clearInterval(interval);
});
</script>
<div class="page">
<div class="wrapper">
<div class="card">
<p class="display">{format(elapsed)}</p>
<div class="buttons">
<button
class={running ? "btn stop" : "btn start"}
on:click={toggle}
>
{running ? "Stop" : "Start"}
</button>
<button class="btn secondary" on:click={lap} disabled={!running}>Lap</button>
<button class="btn secondary" on:click={reset}>Reset</button>
</div>
</div>
{#if laps.length > 0}
<div class="laps">
<div class="laps-scroll">
{#each laps as lapTime, i}
<div class="lap-row">
<span class="lap-label">Lap {laps.length - i}</span>
<span class="lap-time">{format(lapTime)}</span>
</div>
{/each}
</div>
</div>
{/if}
</div>
</div>
<style>
.page {
min-height: 100vh;
background: #0d1117;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.wrapper {
width: 100%;
max-width: 384px;
}
.card {
background: #161b22;
border: 1px solid #30363d;
border-radius: 1rem;
padding: 2rem;
text-align: center;
margin-bottom: 1rem;
}
.display {
font-family: monospace;
font-size: 52px;
font-weight: 700;
color: #e6edf3;
font-variant-numeric: tabular-nums;
line-height: 1;
margin: 0 0 1.5rem;
}
.buttons {
display: flex;
gap: 0.75rem;
justify-content: center;
}
.btn {
flex: 1;
padding: 0.625rem 0;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn.start {
background: #238636;
border: 1px solid #2ea043;
color: white;
}
.btn.start:hover { background: #2ea043; }
.btn.stop {
background: rgba(248, 81, 73, 0.1);
border: 1px solid rgba(248, 81, 73, 0.3);
color: #f85149;
}
.btn.stop:hover { background: rgba(248, 81, 73, 0.2); }
.btn.secondary {
background: #21262d;
border: 1px solid #30363d;
color: #8b949e;
}
.btn.secondary:hover { color: #e6edf3; }
.btn.secondary:disabled { opacity: 0.4; cursor: not-allowed; }
.laps {
background: #161b22;
border: 1px solid #30363d;
border-radius: 0.75rem;
overflow: hidden;
}
.laps-scroll {
max-height: 192px;
overflow-y: auto;
}
.lap-row {
display: flex;
justify-content: space-between;
padding: 0.625rem 1rem;
font-size: 0.875rem;
border-bottom: 1px solid #21262d;
}
.lap-row:last-child { border-bottom: none; }
.lap-label { color: #8b949e; }
.lap-time {
font-family: monospace;
color: #e6edf3;
font-variant-numeric: tabular-nums;
}
</style>Stopwatch
A high-performance stopwatch component designed for accuracy and ease of use. It includes lap timing capabilities and a clear, readable display.
Features
- Millisecond precision
- Lap time tracking with history
- Start, Stop, Lap, and Reset functionality
- Modern, clean UI