DEV Community

Cover image for The Pomodoro Timer Isn’t About Time, It’s About Engineering
YASHWANTH REDDY K
YASHWANTH REDDY K

Posted on

The Pomodoro Timer Isn’t About Time, It’s About Engineering

At first glance, this looks like a design problem.

Make it circular. Make it smooth. Add some pastel colors and a calming font.

Maybe animate an SVG stroke so it “feels” like time is passing.

That’s where most implementations land.

And that’s exactly why most Pomodoro timers feel… slightly off.

Not broken. Just… untrustworthy.

Because the moment you switch tabs and come back — the illusion collapses.

This isn’t a timer UI

It’s a time reconciliation system

What you’re really building here is not a countdown.

You’re building a system that answers one question:

“What time is it supposed to be right now?”

Not:

“How many seconds have I counted?”

That difference is subtle.

And it’s where most AI-generated solutions quietly fail.

The illusion of setInterval

This is the default move:

setInterval(() => {
  timeLeft--;
}, 1000);
Enter fullscreen mode Exit fullscreen mode

It looks correct.

It behaves correctly… as long as the tab is active.

But browsers don’t respect your timer.

They throttle it. Pause it. Delay it.

So your “1 second” becomes:

  • 1.3 seconds
  • 2 seconds
  • sometimes… nothing at all

And your UI keeps pretending everything is fine.

That’s the bug.

Not visible. Not crashing.

Just drifting.

Where it gets interesting

A human usually doesn’t try to “fix” setInterval.

They abandon the idea entirely.

Instead of counting time…

They anchor it.

let endTime = Date.now() + duration;
Enter fullscreen mode Exit fullscreen mode

Now everything changes.

You’re no longer tracking ticks.

You’re calculating truth.

The timer becomes a projection

Every frame, you ask:

const remaining = endTime - Date.now();
Enter fullscreen mode Exit fullscreen mode

That’s it.

No drift.

No accumulation error.

No dependency on browser behavior.

Just a recalculation.

And suddenly:

  • tab switching doesn’t matter
  • lag doesn’t matter
  • animation stays honest

This is the moment the system becomes real.

The orbit isn’t visual

It’s mathematical

The circular progress bar looks like a styling challenge.

It isn’t.

It’s a mapping problem:

time → circumference

That mapping is where most implementations get shaky.

Because they treat the circle like decoration.

Instead of a visual encoding of state

A small line that carries the entire illusion

const progress = remaining / duration;
Enter fullscreen mode Exit fullscreen mode

That ratio drives everything:

  • stroke offset
  • animation smoothness
  • perception of time

If this value is wrong — even slightly — the user feels it.

Not consciously.

But enough to break immersion.

HTML (nothing fancy, and that’s the point)

<div class="app">
  <div class="orbit-container">
    <svg class="orbit" viewBox="0 0 120 120">
      <circle cx="60" cy="60" r="54" class="bg"></circle>
      <circle cx="60" cy="60" r="54" class="progress"></circle>
    </svg>
    <div class="time" id="time">25:00</div>
  </div>

  <div class="controls">
    <button id="focus">Focus</button>
    <button id="chill">Chill</button>
    <button id="start">Start</button>
    <button id="pause">Pause</button>
    <button id="reset">Reset</button>
  </div>

  <div id="message" class="message hidden">Time to breathe</div>
</div>
Enter fullscreen mode Exit fullscreen mode

The structure is intentionally quiet.

Because the behavior is doing the heavy lifting.

CSS (this is where most people over-design)

body {
margin: 0;
font-family: "Inter", sans-serif;
background: #f4f1ee;
color: #2a2a2a;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}

.app {
text-align: center;
}

.orbit-container {
position: relative;
width: 200px;
height: 200px;
margin: auto;
}

.orbit {
transform: rotate(-90deg);
}

circle {
fill: none;
stroke-width: 8;
}

.bg {
stroke: #e0dcd7;
}

.progress {
stroke: #a3c9a8;
stroke-linecap: round;
stroke-dasharray: 339.292;
stroke-dashoffset: 0;
transition: stroke 0.3s ease;
}

.time {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 28px;
font-weight: 500;
}

.controls {
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: center;
}

button {
padding: 10px 14px;
border: none;
border-radius: 20px;
background: #e8e3dd;
cursor: pointer;
}

.message {
margin-top: 20px;
opacity: 0;
transition: opacity 0.5s ease;
}

.message.show {
opacity: 1;
animation: pulse 2s infinite;
}

@keyframes pulse {
0% { opacity: 0.3; }
50% { opacity: 1; }
100% { opacity: 0.3; }
}
Enter fullscreen mode Exit fullscreen mode

The mistake AI models often make here is excess.

Too many gradients. Too much glow.

But “zen” UI is about restraint.

The absence of noise.

JavaScript (this is where the system lives)

const FULL_DASH = 339.292;

let duration = 25 * 60 * 1000;
let endTime = null;
let timerRunning = false;

const timeEl = document.getElementById("time");
const progressEl = document.querySelector(".progress");
const messageEl = document.getElementById("message");

function format(ms) {
const total = Math.max(0, Math.floor(ms / 1000));
const m = String(Math.floor(total / 60)).padStart(2, "0");
const s = String(total % 60).padStart(2, "0");
return `${m}:${s}`;
}

function update() {
if (!timerRunning) return;

const remaining = endTime - Date.now();

if (remaining <= 0) {
timerRunning = false;
timeEl.textContent = "00:00";
progressEl.style.strokeDashoffset = FULL_DASH;
messageEl.classList.add("show");
return;
}

const progress = remaining / duration;

progressEl.style.strokeDashoffset = FULL_DASH * (1 - progress);
timeEl.textContent = format(remaining);

requestAnimationFrame(update);
}

document.getElementById("start").onclick = () => {
if (!endTime) {
endTime = Date.now() + duration;
} else {
endTime = Date.now() + (endTime - Date.now());
}
timerRunning = true;
messageEl.classList.remove("show");
update();
};

document.getElementById("pause").onclick = () => {
timerRunning = false;
duration = endTime - Date.now();
};

document.getElementById("reset").onclick = () => {
timerRunning = false;
endTime = null;
duration = 25 * 60 * 1000;
timeEl.textContent = "25:00";
progressEl.style.strokeDashoffset = 0;
messageEl.classList.remove("show");
};

document.getElementById("focus").onclick = () => {
duration = 25 * 60 * 1000;
endTime = null;
timeEl.textContent = "25:00";
};

document.getElementById("chill").onclick = () => {
duration = 5 * 60 * 1000;
endTime = null;
timeEl.textContent = "05:00";
};
Enter fullscreen mode Exit fullscreen mode

AI vs AI vs Human — where the split happens

This challenge looks harmless.

But in Vibe Code Arena, it exposes something deeper.

Model 1: Animation-first thinking

  • Gets the orbit working
  • Uses setInterval
  • Feels smooth… initially

Until:

  • tab switch breaks timing
  • animation desyncs from reality

It optimizes for appearance, not truth.

Model 2: Over-correcting

  • Uses timestamps
  • But introduces complex state handling
  • Adds unnecessary abstractions

It becomes harder to reason about than the problem itself.

The human approach

Usually lands somewhere quieter:

  • one source of truth → endTime
  • everything derived from Date.now()
  • animation tied to reality, not ticks

Not because it’s “clean”

But because it’s trustworthy

What most people miss

This isn’t about Pomodoro.

It’s about temporal systems

Anywhere you deal with:

  • timers
  • progress
  • countdowns
  • animations tied to time

You face the same choice:

Do you simulate time… or reference it?

Simulation drifts.

Reference stays anchored.

The Reality

After building this, you stop seeing:

“a circular timer UI”

And start seeing:

“a continuously reconciled system between real time and visual state”

And once that clicks…

You start noticing how many apps quietly get this wrong.

If you want to see how different models handle this (and where they drift), try this exact challenge in AI duel mode on Vibe Code Arena.

That’s where “working code” and “reliable systems” stop being the same thing.

👉 Try it out here: https://vibecodearena.ai/share/d8d9208b-9adc-4d11-91ab-d893900f4222

Top comments (0)