Source Code

<div class="pcd-wrap">
  <div class="pcd-tabs" id="pcdTabs">
    <button class="pcd-tab pcd-tab-active" data-mode="focus" data-mins="25">Focus</button>
    <button class="pcd-tab" data-mode="short" data-mins="5">Short Break</button>
    <button class="pcd-tab" data-mode="long" data-mins="15">Long Break</button>
  </div>

  <div class="pcd-display" id="pcdDisplay">25:00</div>
  <div class="pcd-status" id="pcdStatus">Ready to focus</div>

  <div class="pcd-controls">
    <button class="pcd-btn pcd-btn-primary" id="pcdStartBtn">Start</button>
    <button class="pcd-btn" id="pcdResetBtn">Reset</button>
  </div>
</div>

Pomodoro Countdown Timer — Free HTML CSS JS Snippet

Pomodoro Countdown Timer · Misc · Plain HTML, CSS & JS · Live preview

What's included

Features

Drift-free countdown computed from a target end-timestamp (Date.now() + duration) each tick, not naive decrementing
Three modes — Focus (25m), Short Break (5m), Long Break (15m) — sharing one timing engine
Start/Pause toggle that preserves exact remaining time across pause/resume cycles
Reset button that restores the currently selected mode's full duration
"Time's up!" state with a visually distinct red countdown display
Automatic advance to the logical next mode a moment after a session completes
Tabular numeral formatting so the MM: SS display doesn't jitter in width as digits change
Fully self-contained vanilla JS with no external timer library

About this UI Snippet

Pomodoro Countdown Timer — Drift-Free Focus and Break Countdown

Screenshot of the Pomodoro Countdown Timer snippet rendered live

A naive countdown timer that just decrements a counter every second inside setInterval will drift over time, because setInterval isn't guaranteed to fire exactly every 1000ms — background tabs, GC pauses, and CPU load all introduce small delays that accumulate into visible skew over a 25-minute session. This timer avoids that by never trusting the interval's timing directly.

Timestamp-based countdown instead of a decrementing counter

When the timer starts, start() computes endTime = Date.now() + remainingMs once. From then on, every tick (running every 250ms for a responsive display) recomputes remainingMs = endTime - Date.now() fresh from the wall clock, rather than subtracting a fixed amount each tick. Because the remaining time is always derived from two real timestamps, any delay in when a particular tick actually fires has no effect on accuracy — the displayed time is always correct at the moment it's read.

Pause preserves exact remaining time

Pausing calls clearInterval and recomputes remainingMs one final time from endTime - Date.now() before stopping, so resuming later calls start() again with that exact remaining value and computes a fresh endTime — there's no accumulated rounding error across multiple pause/resume cycles.

Three modes, one shared engine

Focus (25 min), Short Break (5 min), and Long Break (15 min) are just three different durationMs values applied to the same setMode()/start()/tick() functions — switching tabs resets remainingMs to the new mode's duration and updates the active tab styling, but uses identical timing logic underneath.

Auto-advance on completion

When remainingMs reaches zero, the timer shows "Time's up!" and, after a short pause, automatically switches to the logically next mode (focus after a break, short break after a focus session) via a lookup table, so a full focus/break cycle can run without the user manually re-selecting a tab each time.

Step by step

How to Use

  1. 1
    Load the snippetClick "Pomodoro Countdown Timer" 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

Productivity and focus-tracking apps
A ready-made Pomodoro technique timer for any focus or study tool.
Browser extensions and desktop productivity widgets
Drop-in timer logic that stays accurate even if the tab is backgrounded.
Teaching drift-free timer implementation
A clear, practical example of why timestamp-based countdowns beat naive decrementing counters.
Study and habit-tracking dashboards
A clean, minimal timer component that fits into a broader personal productivity UI.

Got questions?

Frequently Asked Questions

setInterval callbacks are not guaranteed to fire at exactly the requested interval — delays accumulate over a 25-minute session and cause visible drift. By recomputing remainingMs as endTime - Date.now() on every tick, the displayed time is always correct regardless of exactly when a given tick fires.

Pausing captures the exact remaining milliseconds (endTime - Date.now()) and stops the interval. Resuming computes a brand new endTime from that exact remaining value, so no time is lost or gained across multiple pause/resume cycles.

Yes — when a session reaches zero, after a short "Time's up!" pause it automatically switches to the logically next mode (a break after focus, focus after a break) using a lookup table, though you can start or reset manually at any time.

Yes — edit the data-mins attribute on each tab button in the HTML (and the fallback minute values in the auto-advance logic in the JS) to whatever durations you want for each mode.