Break Timer

A self-hosted work/break interval timer: no app, no account, no ads mid-countdown. Use it right here, or copy the code below into your own project.

Work 25:00 Cycle 1

Why Work/Break Intervals Work

Attention doesn't hold steady for hours at a stretch, it degrades gradually, and the drop is easy to miss from the inside because the work still feels productive right up until it isn't. A fixed work/break interval forces a stopping point before that decline sets in, instead of leaving it to whenever you happen to notice you've been staring at the same line for ten minutes.

The 25/5 Default (and When to Change It)

25 minutes of work against a 5 minute break, the classic Pomodoro ratio, is the default here because it's short enough that starting never feels like a big commitment. Deep, uninterrupted tasks, writing, debugging something gnarly, often do better stretched to 50/10, while shallow, high-interruption work can shrink to 15/5. The point of exposing both fields is that the ratio matters more than the exact numbers, keep breaks at roughly a fifth of the work block.

Why the Beep Matters

A silent timer gets ignored the moment you're absorbed in something, which defeats the purpose. The short tone on each phase change is deliberately built with the Web Audio API rather than an audio file, so there's nothing to host, nothing to load, and nothing that can fail to play because a file didn't fetch in time.

Treat Breaks as Real

The easiest way to waste this timer is to let the break countdown run while you keep working through it. Standing up, looking away from the screen, or just leaving the desk for the five minutes is what actually gives attention room to recover, the timer can enforce the schedule, but it can't enforce what you do with it.

Copy the Code

Plain HTML, CSS, and JavaScript, no dependencies, no build step. Comments walk through what each block is doing, so it's easy to adapt rather than just paste and forget.

<!-- Break Timer -- copy this whole block into any HTML page. -->
<!-- No build step, no dependencies -- plain HTML, CSS, and JS. -->

<div id="break-timer">
  <div class="bt-display">
    <span class="bt-phase" id="btPhase">Work</span>
    <span class="bt-clock" id="btClock">25:00</span>
    <span class="bt-cycle" id="btCycle">Cycle 1</span>
  </div>
  <div class="bt-settings">
    <label>Work (min) <input type="number" id="btWork" value="25" min="1" max="120"></label>
    <label>Break (min) <input type="number" id="btBreak" value="5" min="1" max="60"></label>
  </div>
  <div class="bt-controls">
    <button id="btStart" type="button">Start</button>
    <button id="btReset" type="button">Reset</button>
  </div>
</div>

<style>
  /* Minimal styling -- restyle freely to match your own site. */
  #break-timer { font-family: sans-serif; max-width: 320px; text-align: center; }
  .bt-display { margin-bottom: 12px; }
  .bt-clock { display: block; font-size: 2.5rem; font-weight: 700; }
  .bt-phase { display: block; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; }
  .bt-cycle { display: block; font-size: 0.8rem; opacity: 0.7; }
  .bt-settings { display: flex; gap: 12px; justify-content: center; margin-bottom: 12px; }
  .bt-settings input { width: 60px; }
  .bt-controls button { padding: 8px 16px; margin: 0 4px; cursor: pointer; }
</style>

<script>
(function () {
  // Grab all the elements we need up front.
  var startBtn = document.getElementById('btStart');
  var resetBtn = document.getElementById('btReset');
  var clockEl = document.getElementById('btClock');
  var phaseEl = document.getElementById('btPhase');
  var cycleEl = document.getElementById('btCycle');
  var workInput = document.getElementById('btWork');
  var breakInput = document.getElementById('btBreak');

  // State: which phase we're in, how many seconds are left,
  // and which work/break cycle we're on.
  var phase = 'work';
  var secondsLeft = Number(workInput.value) * 60;
  var cycle = 1;
  var intervalId = null;

  // Turn a raw seconds count into "MM:SS" for display.
  function formatClock(totalSeconds) {
    var m = Math.floor(totalSeconds / 60).toString().padStart(2, '0');
    var s = Math.floor(totalSeconds % 60).toString().padStart(2, '0');
    return m + ':' + s;
  }

  // Push the current state onto the page.
  function render() {
    clockEl.textContent = formatClock(secondsLeft);
    phaseEl.textContent = phase === 'work' ? 'Work' : 'Break';
    cycleEl.textContent = 'Cycle ' + cycle;
  }

  // A short tone on each phase change, built with the Web Audio API
  // so there's no audio file to host or load.
  function beep() {
    try {
      var Ctx = window.AudioContext || window.webkitAudioContext;
      var ctx = new Ctx();
      var osc = ctx.createOscillator();
      var gain = ctx.createGain();
      osc.connect(gain);
      gain.connect(ctx.destination);
      osc.frequency.value = phase === 'work' ? 660 : 880;
      gain.gain.setValueAtTime(0.15, ctx.currentTime);
      osc.start();
      osc.stop(ctx.currentTime + 0.35);
    } catch (err) {
      // If the browser blocks audio (e.g. no user interaction yet), just skip it.
    }
  }

  // Flip from work to break, or break back to work, and load the
  // clock with whichever phase's duration comes next.
  function switchPhase() {
    beep();
    if (phase === 'work') {
      phase = 'break';
      secondsLeft = Number(breakInput.value) * 60;
    } else {
      phase = 'work';
      cycle += 1;
      secondsLeft = Number(workInput.value) * 60;
    }
    render();
  }

  // Runs once a second while the timer is active.
  function tick() {
    secondsLeft -= 1;
    if (secondsLeft <= 0) {
      switchPhase();
      return;
    }
    render();
  }

  function stop() {
    if (intervalId) {
      clearInterval(intervalId);
      intervalId = null;
    }
    startBtn.textContent = 'Start';
  }

  function start() {
    intervalId = setInterval(tick, 1000);
    startBtn.textContent = 'Pause';
  }

  // Start/pause toggle.
  startBtn.addEventListener('click', function () {
    if (intervalId) {
      stop();
    } else {
      start();
    }
  });

  // Reset always goes back to a fresh work phase, cycle 1.
  resetBtn.addEventListener('click', function () {
    stop();
    phase = 'work';
    cycle = 1;
    secondsLeft = Number(workInput.value) * 60;
    render();
  });

  // Let changing the inputs update the clock immediately, but only
  // if the timer isn't running and we're in the matching phase --
  // otherwise you'd yank time out from under an active countdown.
  workInput.addEventListener('change', function () {
    if (!intervalId && phase === 'work') {
      secondsLeft = Number(workInput.value) * 60;
      render();
    }
  });
  breakInput.addEventListener('change', function () {
    if (!intervalId && phase === 'break') {
      secondsLeft = Number(breakInput.value) * 60;
      render();
    }
  });

  render();
})();
</script>