task timer inline time-tracking productivity stopwatch elapsed-time row inline timer time tracking task row elapsed time stopwatch start stop productivity start a timer from a task row track time directly in a task list inline stopwatch per task
Task Row with Inline Timer
Fetch pattern JSON:
curl https://webspire.de/patterns/task-timer-row/base.json base.html
<!-- Task Row with Inline Timer — start/stop per task, only one runs at a time -->
<div class="ws-task-timer-row bg-[var(--ws-task-timer-row-bg)] rounded-2xl border border-slate-200 overflow-hidden" style="font-family:system-ui,sans-serif">
<!-- Header -->
<div class="flex items-center justify-between px-4 py-3 border-b border-slate-100">
<h3 class="text-xs font-bold uppercase tracking-widest text-slate-400">Aufgaben</h3>
<span class="text-xs text-slate-400" id="total-tracked">Heute: 1h 23min</span>
</div>
<!-- Task list -->
<ul class="divide-y divide-slate-50" id="task-list" role="list">
<!-- Task 1 — running -->
<li class="flex items-center gap-3 px-4 py-3 bg-blue-50/50" data-task-id="t1" data-elapsed="3021">
<input type="checkbox" class="w-4 h-4 rounded border-slate-300 accent-blue-500 flex-shrink-0 cursor-pointer" aria-label="Aufgabe erledigt">
<span class="flex-1 text-sm text-slate-800 truncate">API-Dokumentation schreiben</span>
<span class="text-xs tabular-nums font-medium text-blue-600 w-14 text-right timer-display" aria-live="polite">00:50:21</span>
<button
class="w-7 h-7 rounded-full flex items-center justify-center text-xs transition-colors bg-blue-500 text-white hover:bg-blue-600 timer-btn"
aria-label="Timer stoppen" data-running="true">
■
</button>
</li>
<!-- Task 2 — stopped with time -->
<li class="flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors" data-task-id="t2" data-elapsed="1980">
<input type="checkbox" class="w-4 h-4 rounded border-slate-300 accent-blue-500 flex-shrink-0 cursor-pointer" aria-label="Aufgabe erledigt">
<span class="flex-1 text-sm text-slate-800 truncate">Code Review abschließen</span>
<span class="text-xs tabular-nums font-medium text-slate-400 w-14 text-right timer-display">00:33:00</span>
<button
class="w-7 h-7 rounded-full flex items-center justify-center text-xs transition-colors border border-slate-200 text-slate-500 hover:border-blue-400 hover:text-blue-500 timer-btn"
aria-label="Timer starten" data-running="false">
▶
</button>
</li>
<!-- Task 3 — untouched -->
<li class="flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors" data-task-id="t3" data-elapsed="0">
<input type="checkbox" class="w-4 h-4 rounded border-slate-300 accent-blue-500 flex-shrink-0 cursor-pointer" aria-label="Aufgabe erledigt">
<span class="flex-1 text-sm text-slate-800 truncate">E-Mails beantworten</span>
<span class="text-xs tabular-nums font-medium text-slate-300 w-14 text-right timer-display">00:00:00</span>
<button
class="w-7 h-7 rounded-full flex items-center justify-center text-xs transition-colors border border-slate-200 text-slate-500 hover:border-blue-400 hover:text-blue-500 timer-btn"
aria-label="Timer starten" data-running="false">
▶
</button>
</li>
<!-- Task 4 — done -->
<li class="flex items-center gap-3 px-4 py-3 opacity-50" data-task-id="t4" data-elapsed="2700">
<input type="checkbox" checked class="w-4 h-4 rounded border-slate-300 accent-blue-500 flex-shrink-0 cursor-pointer" aria-label="Aufgabe erledigt">
<span class="flex-1 text-sm text-slate-500 truncate line-through">Team-Meeting vorbereiten</span>
<span class="text-xs tabular-nums font-medium text-slate-300 w-14 text-right timer-display">00:45:00</span>
<button class="w-7 h-7 rounded-full flex items-center justify-center text-xs border border-slate-100 text-slate-300 cursor-not-allowed timer-btn" disabled>▶</button>
</li>
</ul>
</div>
<script>
let activeId = 't1'; // t1 starts running in this example
let intervals = {};
const elapsedMap = {};
// Init elapsed from data attributes
document.querySelectorAll('[data-task-id]').forEach(row => {
elapsedMap[row.dataset.taskId] = parseInt(row.dataset.elapsed || '0', 10);
});
function fmt(s) {
const h = String(Math.floor(s / 3600)).padStart(2,'0');
const m = String(Math.floor((s % 3600) / 60)).padStart(2,'0');
const sec = String(s % 60).padStart(2,'0');
return `${h}:${m}:${sec}`;
}
function startTimer(id) {
if (intervals[id]) return;
intervals[id] = setInterval(() => {
elapsedMap[id] = (elapsedMap[id] || 0) + 1;
const row = document.querySelector(`[data-task-id="${id}"]`);
const disp = row?.querySelector('.timer-display');
if (disp) disp.textContent = fmt(elapsedMap[id]);
}, 1000);
}
function stopTimer(id) {
clearInterval(intervals[id]);
delete intervals[id];
}
function setRunningState(id, running) {
const row = document.querySelector(`[data-task-id="${id}"]`);
if (!row) return;
const btn = row.querySelector('.timer-btn');
if (running) {
row.classList.add('bg-blue-50/50', '');
btn.innerHTML = '■';
btn.className = 'w-7 h-7 rounded-full flex items-center justify-center text-xs transition-colors bg-blue-500 text-white hover:bg-blue-600 timer-btn';
btn.setAttribute('aria-label', 'Timer stoppen');
btn.dataset.running = 'true';
const disp = row.querySelector('.timer-display');
if (disp) disp.classList.replace('text-slate-400', 'text-blue-600');
} else {
row.classList.remove('bg-blue-50/50', '');
btn.innerHTML = '▶';
btn.className = 'w-7 h-7 rounded-full flex items-center justify-center text-xs transition-colors border border-slate-200 text-slate-500 hover:border-blue-400 hover:text-blue-500 timer-btn';
btn.setAttribute('aria-label', 'Timer starten');
btn.dataset.running = 'false';
}
}
// Start initial running task
startTimer('t1');
document.getElementById('task-list')?.addEventListener('click', (e) => {
const btn = e.target.closest('.timer-btn');
if (!btn || btn.disabled) return;
const row = btn.closest('[data-task-id]');
const id = row?.dataset.taskId;
if (!id) return;
const isRunning = btn.dataset.running === 'true';
if (isRunning) {
stopTimer(id);
setRunningState(id, false);
activeId = null;
} else {
// Stop current
if (activeId) { stopTimer(activeId); setRunningState(activeId, false); }
startTimer(id);
setRunningState(id, true);
activeId = id;
}
});
</script>
Details
Responsive Dark Mode Tailwind Only Copy & Paste Requires JS
Stable Published
tasktimerinlinetime-trackingproductivitystopwatchelapsed-timerow
Slots
| Name | Required | Description |
|---|---|---|
| task-row | Yes | Each row: checkbox · title · elapsed time · start/stop button. Only one timer runs at a time. |
Props
| Name | Type | Default | Description |
|---|---|---|---|
| exclusive | boolean | true | If true, starting a timer on one row stops any other running timer. |
A compact task list that integrates per-task time tracking directly into each row. Each task shows an elapsed time counter and a play/stop toggle. Only one timer runs at a time — activating a new row automatically stops the previous one. Useful for billable-hour tracking, focus session logging, or any workflow where you need to measure time per item without leaving the task list.