Session Timer + Break Reminder
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.