webdevpuneet.com

Countdown Timer — Free HTML CSS JS Pomodoro Snippet

Countdown Timer · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

Copy & paste

Source Code

<div class="wrap">
  <div class="timer-card">
    <div class="ring-wrap">
      <svg class="ring-svg" viewBox="0 0 120 120">
        <circle class="ring-bg"   cx="60" cy="60" r="52"/>
        <circle class="ring-fill" cx="60" cy="60" r="52" id="ring"/>
      </svg>
      <div class="ring-inner">
        <div class="time-num" id="time-num">25:00</div>
        <div class="time-lbl" id="time-lbl">Focus</div>
      </div>
    </div>

    <div class="mode-tabs">
      <button class="mode active" onclick="setMode('focus',25)" id="btn-focus">Focus</button>
      <button class="mode" onclick="setMode('break',5)"  id="btn-break">Short break</button>
      <button class="mode" onclick="setMode('long',15)"  id="btn-long">Long break</button>
    </div>

    <div class="controls">
      <button class="ctrl-btn" id="main-btn" onclick="toggle()">Start</button>
      <button class="ctrl-btn secondary" onclick="reset()">Reset</button>
    </div>

    <div class="session-row">
      Session <strong id="session-num">1</strong> of 4
      <div class="dots" id="dots">
        <span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="dot"></span>
      </div>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

SVG stroke-dashoffset ring: C=2πr, dashoffset=C*(1-remaining/total), drains as time passes
Ring colour per mode: indigo (focus), green (short break), cyan (long break)
Start/Pause/Resume: running boolean + clearInterval/setInterval toggle pattern
Reset: clears interval, restores remaining to totalSecs, resets button label
Session dot tracker: 4 dots fill as focus sessions complete
font-variant-numeric: tabular-nums — stable digit width prevents layout shift
CSS transition: stroke-dashoffset 0.5s linear — smooth per-second ring update
Three Pomodoro mode tabs: Focus/Short break/Long break with shared setMode()

About this UI Snippet

Countdown Timer — SVG Ring Progress, Pomodoro Modes, Session Dots & Pause/Resume

Screenshot of the Countdown Timer snippet rendered live

A countdown timer with a circular progress ring is one of the most visually engaging loading and progress components you can build without a library. This snippet implements a complete Pomodoro-style countdown timer: an SVG stroke-dashoffset ring that drains as time passes, three mode tabs (Focus 25min, Short break 5min, Long break 15min), Start/Pause/Resume/Reset controls, a session counter with dot indicators, and a colour change per mode — all in plain HTML, CSS, and vanilla JavaScript.

How the SVG ring countdown works

The ring uses the stroke-dashoffset technique. The circle has a circumference C = 2 × π × radius = 2 × π × 52 ≈ 327px. stroke-dasharray: 327 makes the stroke one continuous dash equal to the full circumference. stroke-dashoffset controls how much of the dash is hidden — setting it to 327 hides the full ring (empty), 0 shows the full ring (complete). As time passes: dashoffset = C × (1 - remaining/total). The CSS transition: stroke-dashoffset 0.5s linear smooths the movement each second.

The Pomodoro mode tabs

Three tabs switch between Focus (25 min, indigo ring), Short break (5 min, green ring), and Long break (15 min, cyan ring). Switching a mode resets the timer, updates the stroke colour via ring.style.stroke, and updates the centre label. The mode button active state uses the same pill tab pattern as the calendar widget.

Start/Pause/Resume logic

A setInterval fires every 1000ms when running. The toggle() function checks the running boolean: if running, it clears the interval, sets running = false, and changes the button to "Resume". If not running, it starts the interval and changes the button to "Pause". The Reset button clears the interval and restores remaining to totalSecs.

Session dot tracker

Four dot indicators track completed focus sessions. Each completed focus session (when the ring reaches 0 in Focus mode) fills one dot with the accent colour and increments the session counter. After 4 sessions, the counter stops at 4 — a visual cue to take a long break.

Tabular number digits

The time display uses font-variant-numeric: tabular-nums — a CSS OpenType feature that makes all digits the same width, preventing the display from shifting width as the time counts down.

Step by step

How to Use

  1. 1
    Click Start to begin the countdownThe SVG ring starts draining and the timer counts down. Click Pause to pause mid-session. Click Resume to continue. The button label changes to reflect the current state.
  2. 2
    Switch between Focus and break modesClick Focus (25 min), Short break (5 min), or Long break (15 min) to switch modes. The ring colour changes: indigo for Focus, green for Short break, cyan for Long break. Switching resets the timer.
  3. 3
    Change the timer durationsIn the JS panel, update the modes object: { focus: N, break: N, long: N } where N is the duration in minutes. Change the onclick values in the HTML mode buttons to match the new durations.
  4. 4
    Customize ring coloursIn the JS setMode() function, update the ring.style.stroke values per mode. In the CSS, update .ring-fill stroke and .ctrl-btn background to change the default accent colour.
  5. 5
    Add a sound notification on timer endInside the ticker interval, when remaining <= 0, add: const audio = new Audio("bell.mp3"); audio.play(). Or use the Web Audio API to generate a tone: const ctx = new AudioContext(); const osc = ctx.createOscillator(); osc.connect(ctx.destination); osc.start(); setTimeout(() => osc.stop(), 400).
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component using useState for remaining, useEffect for the interval with cleanup, or "Tailwind" for a Tailwind version.

Real-world uses

Common Use Cases

Pomodoro technique productivity timer for focus sessions
The Pomodoro technique (25-minute focus, 5-minute break cycles) is one of the most researched time management methods. This timer implements the complete cycle: 4 focus sessions tracked by dot indicators, with short breaks between and a long break after all 4 sessions — see also the task-list Pomodoro widget.
Meeting and presentation countdown timers
Set the focus duration to your meeting length (30 min, 60 min). The visual ring gives presenters and participants a clear sense of remaining time without checking a phone. Use the short break mode for Q&A periods.
Exam, quiz, and timed challenge interfaces
Wire the timer to a quiz or exam UI. When remaining reaches 0, auto-submit the form. The ring provides a visual urgency cue as time expires without requiring the user to read a number. The ring colour can shift to red as time runs low: if (remaining < 60) ring.style.stroke = "#ef4444".
Browser extension new tab page timer widget
Export the HTML and deploy as a browser new tab override page or a Chrome extension popup. The timer persists in memory while the tab is open. Add localStorage state save on every tick() to persist across tab navigations.
Study stroke-dashoffset mathematics and setInterval timing
The ring countdown demonstrates the SVG stroke-dasharray/dashoffset technique for showing progress percentages on a circular path. The setInterval pattern shows how to implement a precise 1-second countdown with pause/resume control using a single boolean flag.
Cooking timer, workout interval, and meditation apps
The three-mode tab system adapts to workout intervals (Work/Rest/Cooldown), cooking timers (Prep/Cook/Rest), or meditation — pair it with the guided breathing animation for the breathe/hold/release phases. Change the mode labels and durations to match the domain.
Related: Audio Buffering Visualizer
See the Audio Buffering Visualizer for a related loaders pattern worth pairing with this one.
Related: Stage Progress Fill Checklist
See the Stage Progress Fill Checklist for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The ring uses stroke-dasharray: 327 (the full circumference, computed as 2×π×52) and stroke-dashoffset. When dashoffset = 0, the full ring is visible. When dashoffset = 327, the ring is completely hidden. The formula dashoffset = C × (1 - remaining/total) maps the remaining time fraction to the dashoffset: at full time remaining (remaining/total = 1), dashoffset = 0 (full ring). At time expired (remaining/total = 0), dashoffset = C (empty ring). CSS transition: stroke-dashoffset 0.5s linear smooths the change each second.

In the setInterval callback, when remaining <= 0, use the Web Audio API: const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 440; gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.8); osc.start(); osc.stop(ctx.currentTime + 0.8). This creates a 440Hz tone that fades out over 0.8 seconds without any audio file.

Inside the ticker setInterval, after decrementing remaining, add a colour warning: if (remaining <= 60 && currentMode === "focus") { document.getElementById("ring").style.stroke = "#ef4444"; } else if (remaining <= 0) { document.getElementById("ring").style.stroke = ""; }. The CSS transition on the stroke property (add transition: stroke 0.3s to .ring-fill) will animate the colour change smoothly.

Click "JSX" to download. Manage remaining, running, currentMode, and session in useState. Run the interval in useEffect: useEffect(() => { if (!running) return; const id = setInterval(() => setRemaining(r => r - 1), 1000); return () => clearInterval(id); }, [running]). Use useEffect to check if remaining reaches 0: useEffect(() => { if (remaining <= 0) { setRunning(false); if (currentMode === "focus") setSession(s => Math.min(4, s+1)); } }, [remaining]).