HTML
<section class="cdt-cir" aria-label="Circular progress countdown timer demo">
<div class="cdt-cir__panel">
<div class="cdt-cir__quiz-label">Section 2 of 4</div>
<div class="cdt-cir__quiz-title">Algebra Aptitude Quiz</div>
<div class="cdt-cir__ring-wrap">
<svg viewBox="0 0 240 240" aria-hidden="true">
<circle class="cdt-cir__ring-bg" cx="120" cy="120" r="106"/>
<circle class="cdt-cir__ring-fg" data-cdt-cir="ring" cx="120" cy="120" r="106"/>
</svg>
<div class="cdt-cir__ring-center" role="timer" aria-live="polite" aria-atomic="true">
<div class="cdt-cir__time" data-cdt-cir="time">02:00</div>
<div class="cdt-cir__cap">Remaining</div>
</div>
</div>
<div class="cdt-cir__status" data-cdt-cir="status">
<span class="cdt-cir__pip" aria-hidden="true"></span>
<span data-cdt-cir="statusTxt">Time is running</span>
</div>
<div class="cdt-cir__controls">
<button data-cdt-cir="reset" type="button">Reset</button>
<button data-cdt-cir="toggle" type="button" class="cdt-cir__primary">Pause</button>
</div>
</div>
</section> CSS
/* ─── 05 Circular Progress Timer — SVG ring drain ──────────── */
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700;800&family=JetBrains+Mono:wght@600;700&display=swap');
.cdt-cir {
--cdt-cir-bg: #0f1419;
--cdt-cir-panel: #161d26;
--cdt-cir-panel-line: #232c38;
--cdt-cir-text: #e8eef4;
--cdt-cir-muted: #7c8a9a;
--cdt-cir-ring-bg: #232c38;
--cdt-cir-green: #34d399;
--cdt-cir-amber: #fbbf24;
--cdt-cir-red: #f87171;
position: relative;
width: 100%;
min-height: 560px;
background: var(--cdt-cir-bg);
background-image: radial-gradient(circle at 50% 0%, rgba(52,211,153,0.06), transparent 50%);
font-family: 'Sora', sans-serif;
color: var(--cdt-cir-text);
display: flex; align-items: center; justify-content: center;
padding: 36px 20px;
box-sizing: border-box;
overflow: hidden;
}
.cdt-cir *, .cdt-cir *::before, .cdt-cir *::after { box-sizing: border-box; margin: 0; padding: 0; }
.cdt-cir__panel {
background: var(--cdt-cir-panel);
border: 1px solid var(--cdt-cir-panel-line);
border-radius: 28px;
padding: 36px 36px 32px;
width: 100%; max-width: 380px;
text-align: center;
box-shadow: 0 40px 80px -40px rgba(0,0,0,0.8);
}
.cdt-cir__quiz-label {
font-size: 12px; letter-spacing: 2.5px; text-transform: uppercase;
color: var(--cdt-cir-muted); margin-bottom: 6px; font-weight: 600;
}
.cdt-cir__quiz-title { font-size: 18px; font-weight: 700; margin-bottom: 26px; }
.cdt-cir__ring-wrap { position: relative; width: 220px; height: 220px; margin: 0 auto 26px; }
.cdt-cir__ring-wrap svg { transform: rotate(-90deg); width: 100%; height: 100%; }
.cdt-cir__ring-bg { fill: none; stroke: var(--cdt-cir-ring-bg); stroke-width: 14; }
.cdt-cir__ring-fg {
fill: none; stroke: var(--cdt-cir-green); stroke-width: 14; stroke-linecap: round;
transition: stroke-dashoffset 1s linear, stroke 0.4s ease;
filter: drop-shadow(0 0 8px rgba(52,211,153,0.5));
}
.cdt-cir__ring-center {
position: absolute; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
.cdt-cir__time {
font-family: 'JetBrains Mono', monospace;
font-size: 52px; font-weight: 700; line-height: 1; letter-spacing: -1px;
}
.cdt-cir__cap {
font-size: 11px; letter-spacing: 2px; text-transform: uppercase;
color: var(--cdt-cir-muted); margin-top: 8px;
}
.cdt-cir__status {
display: inline-flex; align-items: center; gap: 8px;
font-size: 13px; font-weight: 600;
padding: 7px 16px; border-radius: 20px;
background: rgba(52,211,153,0.12); color: var(--cdt-cir-green);
margin-bottom: 24px; transition: all 0.4s;
}
.cdt-cir__pip {
width: 8px; height: 8px; border-radius: 50%; background: currentColor;
animation: cdt-cir-blip 1.2s infinite;
}
@keyframes cdt-cir-blip { 50% { opacity: 0.3; } }
.cdt-cir__controls { display: flex; gap: 10px; }
.cdt-cir__controls button {
flex: 1;
font-family: 'Sora', sans-serif; font-weight: 600; font-size: 14px;
padding: 12px; border-radius: 14px; cursor: pointer;
border: 1px solid var(--cdt-cir-panel-line);
background: transparent; color: var(--cdt-cir-text);
transition: background 0.2s, transform 0.15s, border-color 0.2s;
}
.cdt-cir__controls button:hover { background: var(--cdt-cir-panel-line); transform: translateY(-1px); }
.cdt-cir__controls button:focus-visible { outline: 2px solid var(--cdt-cir-green); outline-offset: 2px; }
.cdt-cir__primary {
background: var(--cdt-cir-green) !important;
color: #07120d !important;
border-color: var(--cdt-cir-green) !important;
}
.cdt-cir__primary:hover { background: #2bbd86 !important; }
@media (prefers-reduced-motion: reduce) {
.cdt-cir__ring-fg, .cdt-cir__pip, .cdt-cir__controls button { transition: none; animation: none; }
} JS
(() => {
const root = document.querySelector('.cdt-cir');
if (!root) return;
const R = 106;
const CIRC = 2 * Math.PI * R;
const ring = root.querySelector('[data-cdt-cir="ring"]');
const timeEl = root.querySelector('[data-cdt-cir="time"]');
const statusEl = root.querySelector('[data-cdt-cir="status"]');
const statusTxt = root.querySelector('[data-cdt-cir="statusTxt"]');
const toggleBtn = root.querySelector('[data-cdt-cir="toggle"]');
const resetBtn = root.querySelector('[data-cdt-cir="reset"]');
ring.style.strokeDasharray = CIRC;
const TOTAL = 120;
let remaining = TOTAL;
let running = true;
const pad = (n) => String(n).padStart(2, '0');
function color() {
const frac = remaining / TOTAL;
if (frac > 0.5) return '#34d399';
if (frac > 0.2) return '#fbbf24';
return '#f87171';
}
function render() {
timeEl.textContent = pad(Math.floor(remaining / 60)) + ':' + pad(remaining % 60);
ring.style.strokeDashoffset = CIRC * (1 - remaining / TOTAL);
const c = color();
ring.style.stroke = c;
ring.style.filter = 'drop-shadow(0 0 8px ' + c + ')';
statusEl.style.background = c + '20';
statusEl.style.color = c;
if (remaining <= 0) {
statusTxt.textContent = "Time's up!";
running = false;
toggleBtn.textContent = 'Start';
}
}
function loop() {
if (running && remaining > 0) {
remaining--;
render();
}
}
toggleBtn.onclick = () => {
if (remaining <= 0) remaining = TOTAL;
running = !running;
toggleBtn.textContent = running ? 'Pause' : 'Resume';
statusTxt.textContent = running ? 'Time is running' : 'Paused';
};
resetBtn.onclick = () => {
remaining = TOTAL;
running = true;
toggleBtn.textContent = 'Pause';
statusTxt.textContent = 'Time is running';
render();
};
render();
setInterval(loop, 1000);
})();