Session Timer + Break Reminder

December 22, 2025

This is a small, lightweight session timer you can drop onto any page. Includes Start / Pause / Reset and an optional break reminder that nudges after a set number of minutes. No frameworks, no tracking, no storage required.

Newbie setup: paste the code once, then change REMIND_EVERY_MIN if you want reminders (set to 0 to disable).

<!--
Session Timer + Break Reminder (No Frameworks)
- Start / Pause / Reset
- Optional reminder toast every X minutes (set to 0 to disable)
- Scoped styles (won’t mess up site layout)

SETUP:
1) Paste everything below where you want the timer to appear.
2) Change REMIND_EVERY_MIN (0 disables reminders).
-->

<div class="vs-timer" role="region" aria-label="Session timer">
  <div class="vs-timer__top">
    <div class="vs-timer__label">Session Timer</div>
    <div class="vs-timer__time" aria-live="polite">00:00</div>
  </div>

  <div class="vs-timer__row">
    <button type="button" class="vs-timer__btn" data-timer-start>Start</button>
    <button type="button" class="vs-timer__btn" data-timer-pause disabled>Pause</button>
    <button type="button" class="vs-timer__btn" data-timer-reset>Reset</button>
  </div>

  <div class="vs-timer__hint">
    Break reminder: <span data-timer-remind-text>off</span>
  </div>

  <div class="vs-timer__toast" hidden role="status" aria-live="polite"></div>
</div>

<style>
/* Scoped to .vs-timer only */
.vs-timer{
  max-width: 420px;
  border: 1px solid rgba(30,40,80,.14);
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 10px 30px rgba(20,24,70,.08);
  padding: 14px;
  font-family: system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  color: #0c0f14;
}
.vs-timer__top{
  display:flex;
  align-items:baseline;
  justify-content:space-between;
  gap:12px;
}
.vs-timer__label{
  font-weight: 900;
  letter-spacing: .2px;
}
.vs-timer__time{
  font-weight: 900;
  font-size: 22px;
  letter-spacing: .5px;
}
.vs-timer__row{
  display:flex;
  gap:10px;
  margin-top: 12px;
  flex-wrap: wrap;
}
.vs-timer__btn{
  appearance:none;
  border: 1px solid rgba(30,40,80,.14);
  background: rgba(20,24,70,.04);
  color:#0c0f14;
  padding: 9px 12px;
  border-radius: 12px;
  font-weight: 800;
  cursor:pointer;
}
.vs-timer__btn:disabled{
  opacity:.55;
  cursor:not-allowed;
}
.vs-timer__btn:hover:not(:disabled){
  filter: brightness(0.98);
}
.vs-timer__hint{
  margin-top: 10px;
  color: #5a6177;
  font-size: 13px;
}
.vs-timer__toast{
  margin-top: 12px;
  padding: 10px 12px;
  border-radius: 14px;
  border: 1px solid rgba(168,41,167,.25);
  background: rgba(168,41,167,.08);
  color:#0c0f14;
  font-weight: 800;
}
</style>

<script>
(function(){
  "use strict";

  // ===== CONFIG =====
  // Set to 0 to disable reminders.
  var REMIND_EVERY_MIN = 20;

  // ===== ELEMENTS =====
  var root = document.querySelector(".vs-timer");
  if (!root) return;

  var elTime = root.querySelector(".vs-timer__time");
  var btnStart = root.querySelector("[data-timer-start]");
  var btnPause = root.querySelector("[data-timer-pause]");
  var btnReset = root.querySelector("[data-timer-reset]");
  var elToast = root.querySelector("[data-timer-remind-text]") ? root.querySelector(".vs-timer__toast") : null;
  var elRemText = root.querySelector("[data-timer-remind-text]");
  var toastBox = root.querySelector(".vs-timer__toast");

  // ===== STATE =====
  var startMs = 0;
  var elapsedMs = 0;
  var tickId = null;
  var nextReminderAtMs = 0;

  function pad2(n){ return (n < 10 ? "0" : "") + n; }

  function fmt(ms){
    var total = Math.floor(ms / 1000);
    var m = Math.floor(total / 60);
    var s = total % 60;
    return pad2(m) + ":" + pad2(s);
  }

  function setButtons(running){
    btnStart.disabled = running;
    btnPause.disabled = !running;
  }

  function showToast(msg){
    if (!toastBox) return;
    toastBox.textContent = msg;
    toastBox.hidden = false;
    // auto-hide after 4s
    window.clearTimeout(showToast._t);
    showToast._t = window.setTimeout(function(){
      toastBox.hidden = true;
    }, 4000);
  }

  function computeElapsed(){
    if (!startMs) return elapsedMs;
    return elapsedMs + (Date.now() - startMs);
  }

  function scheduleNextReminder(currentMs){
    if (REMIND_EVERY_MIN <= 0) {
      nextReminderAtMs = 0;
      if (elRemText) elRemText.textContent = "off";
      return;
    }
    var interval = REMIND_EVERY_MIN * 60 * 1000;
    // next reminder at the next multiple of interval
    nextReminderAtMs = Math.ceil(currentMs / interval) * interval;
    if (nextReminderAtMs === currentMs) nextReminderAtMs += interval;
    if (elRemText) elRemText.textContent = REMIND_EVERY_MIN + " min";
  }

  function tick(){
    var ms = computeElapsed();
    elTime.textContent = fmt(ms);

    if (REMIND_EVERY_MIN > 0 && nextReminderAtMs > 0 && ms >= nextReminderAtMs) {
      showToast("Break reminder: take a quick pause.");
      scheduleNextReminder(ms);
    }
  }

  function start(){
    if (tickId) return;
    startMs = Date.now();
    setButtons(true);
    scheduleNextReminder(computeElapsed());
    tick();
    tickId = window.setInterval(tick, 250);
  }

  function pause(){
    if (!tickId) return;
    elapsedMs = computeElapsed();
    startMs = 0;
    window.clearInterval(tickId);
    tickId = null;
    setButtons(false);
  }

  function reset(){
    pause();
    elapsedMs = 0;
    startMs = 0;
    elTime.textContent = "00:00";
    if (toastBox) toastBox.hidden = true;
    scheduleNextReminder(0);
  }

  // Init
  setButtons(false);
  scheduleNextReminder(0);

  // Events
  btnStart.addEventListener("click", start);
  btnPause.addEventListener("click", pause);
  btnReset.addEventListener("click", reset);
})();
</script>

Comments (0)

No comments yet — be the first.

← Back to all scripts