Countdown Ring
A timer that draws itself down to zero
Running live from https://fun.whitneys.co,
cross-origin and sandboxed.
Put it on your site
<iframe
src="https://fun.whitneys.co/g/countdown-ring"
width="300"
height="300"
title="Countdown Ring"
sandbox="allow-scripts allow-popups-to-escape-sandbox"
loading="lazy"
style="border:0"></iframe>
An iframe, not a script tag -- so this gadget cannot touch the page you paste it into, and neither can we. Your settings above are baked into the URL.
The whole thing
Raw fileEvery gadget is one file, and this is it -- all 4.1 kB. Take it, change it, ship it. Just keep the credit.
Show source
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<script type="application/json" id="gadget-manifest">
{
"name": "Countdown Ring",
"tagline": "A timer that draws itself down to zero",
"category": "time",
"tags": ["timer", "svg", "focus"],
"order": 1,
"author": "rwhitney",
"background": "transparent",
"size": { "w": 300, "h": 300, "resizable": false },
"config": {
"minutes": { "type": "range", "label": "Minutes", "min": 1, "max": 60, "step": 1, "default": 5 },
"accent": { "type": "color", "label": "Ring", "default": "#A6E22E" },
"style": { "type": "select", "label": "Readout", "options": ["digits", "words"], "default": "digits" },
"autostart": { "type": "bool", "label": "Start immediately", "default": false }
},
"permissions": []
}
</script>
<style>
html, body {
margin: 0; height: 100%;
display: grid; place-items: center;
font: 16px/1.4 ui-sans-serif, system-ui, sans-serif;
background: transparent; color: #F8F8F2;
overflow: hidden; user-select: none;
}
#wrap { display: grid; place-items: center; gap: 10px; }
svg { display: block; transform: rotate(-90deg); }
#track { stroke: rgb(248 248 242 / 0.12); }
#bar { stroke: var(--accent, #A6E22E); transition: stroke-dashoffset 250ms linear; }
#readout {
position: absolute; font-variant-numeric: tabular-nums;
font-size: 34px; font-weight: 700; letter-spacing: -0.02em;
}
#readout.words { font-size: 19px; font-weight: 600; }
#stage { position: relative; display: grid; place-items: center; }
button {
font: inherit; font-weight: 600; font-size: 14px;
padding: 6px 16px; border-radius: 6px; cursor: pointer;
background: transparent; color: var(--accent, #A6E22E);
border: 1px solid currentColor;
}
button:hover { background: rgb(255 255 255 / 0.06); }
#done { color: var(--accent, #A6E22E); }
</style>
</head>
<body>
<div id="wrap">
<div id="stage">
<svg width="200" height="200" viewBox="0 0 200 200" aria-hidden="true">
<circle id="track" cx="100" cy="100" r="88" fill="none" stroke-width="10"/>
<circle id="bar" cx="100" cy="100" r="88" fill="none" stroke-width="10"
stroke-linecap="round"/>
</svg>
<div id="readout" role="timer" aria-live="polite"></div>
</div>
<button id="toggle" type="button">Start</button>
</div>
<script>
(function () {
var cfg = window.gadget ? window.gadget.config : {};
var total = (typeof cfg.minutes === 'number' ? cfg.minutes : 5) * 60;
var accent = cfg.accent || '#A6E22E';
var words = cfg.style === 'words';
document.documentElement.style.setProperty('--accent', accent);
var bar = document.getElementById('bar');
var readout = document.getElementById('readout');
var toggle = document.getElementById('toggle');
var CIRC = 2 * Math.PI * 88;
bar.style.strokeDasharray = CIRC;
if (words) readout.classList.add('words');
var left = total, running = false, last = 0, raf = 0;
function fmt(s) {
if (!words) {
var m = Math.floor(s / 60), r = Math.floor(s % 60);
return m + ':' + String(r).padStart(2, '0');
}
if (s <= 0) return 'done';
var mm = Math.ceil(s / 60);
if (s < 60) return Math.ceil(s) + ' sec';
return mm + (mm === 1 ? ' minute' : ' minutes');
}
function paint() {
bar.style.strokeDashoffset = CIRC * (1 - left / total);
readout.textContent = fmt(left);
}
function tick(now) {
if (!running) return;
if (last) left = Math.max(0, left - (now - last) / 1000);
last = now;
paint();
if (left <= 0) {
running = false;
toggle.textContent = 'Reset';
readout.id = 'done';
return;
}
raf = requestAnimationFrame(tick);
}
toggle.addEventListener('click', function () {
if (left <= 0) {
left = total; paint();
toggle.textContent = 'Start';
return;
}
running = !running;
toggle.textContent = running ? 'Pause' : 'Start';
last = 0;
if (running) raf = requestAnimationFrame(tick);
else cancelAnimationFrame(raf);
});
paint();
if (cfg.autostart) toggle.click();
})();
</script>
</body>
</html>