Freedom Sale
Independence Day Special — Unlock the AI Path 70% off our most popular AI course · Limited time offer
--Days
--Hrs
--Min
--Sec
Claim Your Discount
✦ Beginner ⏱ 35 min

🍅 Build a Pomodoro Timer Web App

🎯 What You'll Build

A beautiful browser Pomodoro timer with a circular progress ring, work/break mode switching, session counter, and a notification alert.

📋 What You'll Need

1

HTML structure and SVG progress ring

The circular timer uses an SVG circle with a stroke-dashoffset animation.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Pomodoro Timer</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>

<?php require_once __DIR__ . '/../promo_banner.php'; ?>

  <div class="app">
    <h1>🍅 Pomodoro Timer</h1>
    <div class="mode-btns">
      <button class="mode-btn active" onclick="setMode('work')">Work</button>
      <button class="mode-btn" onclick="setMode('short')">Short Break</button>
      <button class="mode-btn" onclick="setMode('long')">Long Break</button>
    </div>

    <div class="timer-ring">
      <svg width="220" height="220" viewBox="0 0 220 220">
        <circle class="ring-bg"   cx="110" cy="110" r="96" />
        <circle class="ring-fill" cx="110" cy="110" r="96" id="ring" />
      </svg>
      <div class="timer-display" id="display">25:00</div>
    </div>

    <div class="controls">
      <button id="startBtn" onclick="toggle()">Start</button>
      <button onclick="reset()">Reset</button>
    </div>
    <div class="sessions">Sessions completed: <b id="sessionCount">0</b></div>
  </div>
  <script src="app.js"></script>
</body>
</html>
2

CSS — ring animation and layout

The ring animates by changing stroke-dashoffset on the SVG circle.

/* style.css */
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Segoe UI',sans-serif; background:#0f172a; color:#fff; min-height:100vh; display:flex; align-items:center; justify-content:center; }
.app { text-align:center; }
h1 { font-size:1.6rem; margin-bottom:24px; }

.mode-btns { display:flex; gap:10px; justify-content:center; margin-bottom:32px; }
.mode-btn { padding:8px 18px; border:2px solid #334155; background:none; color:#94a3b8; border-radius:20px; cursor:pointer; font-size:0.85rem; transition:all 0.2s; }
.mode-btn.active { background:#e74c3c; border-color:#e74c3c; color:#fff; font-weight:700; }

.timer-ring { position:relative; width:220px; height:220px; margin:0 auto 32px; }
.ring-bg   { fill:none; stroke:#1e293b; stroke-width:12; }
.ring-fill { fill:none; stroke:#e74c3c; stroke-width:12; stroke-linecap:round;
  stroke-dasharray: 603; stroke-dashoffset: 0;
  transform:rotate(-90deg); transform-origin:50% 50%;
  transition: stroke-dashoffset 1s linear, stroke 0.3s; }
.ring-fill.break { stroke:#22c55e; }
.timer-display { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); font-size:3rem; font-weight:800; font-variant-numeric:tabular-nums; }

.controls { display:flex; gap:16px; justify-content:center; margin-bottom:20px; }
.controls button { padding:12px 36px; border:none; border-radius:10px; font-size:1rem; font-weight:700; cursor:pointer; }
#startBtn { background:#e74c3c; color:#fff; }
.controls button:last-child { background:#1e293b; color:#94a3b8; }
.sessions { color:#64748b; font-size:0.88rem; }
3

JavaScript timer logic

Track elapsed time, update the display, and animate the ring.

// app.js
const MODES = { work:25*60, short:5*60, long:15*60 };
const CIRCUMFERENCE = 2 * Math.PI * 96; // ~603

let currentMode = 'work';
let totalSecs, remaining, interval, running = false, sessions = 0;

function setMode(mode) {
  clearInterval(interval); running = false;
  document.getElementById('startBtn').textContent = 'Start';
  document.querySelectorAll('.mode-btn').forEach((b,i) =>
    b.classList.toggle('active', ['work','short','long'][i] === mode));
  const ring = document.getElementById('ring');
  ring.classList.toggle('break', mode !== 'work');
  document.getElementById('startBtn').style.background = mode === 'work' ? '#e74c3c' : '#22c55e';
  currentMode = mode;
  totalSecs = remaining = MODES[mode];
  updateDisplay();
  updateRing(1);
}

function updateDisplay() {
  const m = Math.floor(remaining / 60);
  const s = remaining % 60;
  document.getElementById('display').textContent =
    `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
  document.title = `${document.getElementById('display').textContent} — Pomodoro`;
}

function updateRing(fraction) {
  const offset = CIRCUMFERENCE * (1 - fraction);
  document.getElementById('ring').style.strokeDashoffset = offset;
}

function toggle() {
  if (running) {
    clearInterval(interval); running = false;
    document.getElementById('startBtn').textContent = 'Resume';
  } else {
    running = true;
    document.getElementById('startBtn').textContent = 'Pause';
    interval = setInterval(() => {
      remaining--;
      updateDisplay();
      updateRing(remaining / totalSecs);
      if (remaining <= 0) {
        clearInterval(interval); running = false;
        if (currentMode === 'work') {
          sessions++;
          document.getElementById('sessionCount').textContent = sessions;
        }
        notify(currentMode === 'work' ? 'Work session done! Take a break.' : 'Break over! Time to work.');
      }
    }, 1000);
  }
}

function reset() {
  clearInterval(interval); running = false;
  document.getElementById('startBtn').textContent = 'Start';
  remaining = MODES[currentMode];
  updateDisplay(); updateRing(1);
}

function notify(msg) {
  if (Notification.permission === 'granted') {
    new Notification('🍅 Pomodoro', { body: msg });
  } else {
    alert(msg);
  }
}

// Request notification permission on load
if (Notification.permission === 'default') Notification.requestPermission();

setMode('work');
💡 Tip: The SVG circle radius is 96px, giving circumference = 2π×96 ≈ 603px. We animate stroke-dashoffset from 0 (full ring) to 603 (empty ring) as time counts down.

🎉 You Did It!

A polished productivity tool with zero dependencies. The same SVG ring technique is used for loading spinners, progress bars, and skill charts on CVs.

Found something wrong?

Spotted a bug, broken code, or something that doesn't look right? Tell us what's off and we'll fix it.