Contagem regressiva de atividades
Cronômetro regressivo configurável em minutos, com suporte a pausar, continuar e reiniciar.
É possível editar, executar e salvar este exemplo após ativar JavaScript. O código-fonte fixo é o seguinte:
<!doctype html>
<html lang="pt-BR" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Contagem regressiva de atividades</title>
<style>
* { box-sizing: border-box; }
:root { color-scheme: dark; }
body { min-height: 100vh; margin: 0; display: grid; place-items: center; padding: 24px; color: #fff8e8; font-family: "Avenir Next", "Microsoft YaHei", sans-serif; background: radial-gradient(circle at 20% 20%, rgba(255,125,75,.17), transparent 32rem), #17151c; }
.timer-card { width: min(680px, 100%); padding: clamp(28px, 6vw, 56px); border: 1px solid #38333f; border-radius: 34px; background: #211e27; box-shadow: 0 32px 90px rgba(0,0,0,.38); text-align: center; }
.eyebrow { color: #ff8258; font-size: 11px; font-weight: 900; letter-spacing: .24em; }
h1 { margin: 10px 0 6px; font: 600 clamp(34px, 7vw, 56px)/1 Georgia, "STSong", serif; }
.subtitle { margin: 0; color: #97909e; font-size: 14px; }
.clock { position: relative; width: min(310px, 72vw); aspect-ratio: 1; margin: 32px auto; display: grid; place-items: center; }
svg { position: absolute; inset: 0; width: 100%; transform: rotate(-90deg); }
circle { fill: none; stroke-width: 8; }
.track { stroke: #342f3a; }
.progress { stroke: #ff8258; stroke-linecap: round; stroke-dasharray: 603.19; stroke-dashoffset: 0; filter: drop-shadow(0 0 6px rgba(255,130,88,.55)); transition: stroke-dashoffset .25s linear; }
.time { font: 500 clamp(48px, 12vw, 76px)/1 "Courier New", monospace; letter-spacing: -.07em; font-variant-numeric: tabular-nums; }
.state { display: block; margin-top: 12px; color: #9f98a7; font-size: 11px; font-weight: 800; letter-spacing: .15em; }
.settings { display: flex; justify-content: center; gap: 12px; align-items: center; margin-bottom: 24px; }
.settings label { color: #aba4b1; font-size: 13px; }
input { width: 88px; padding: 10px 12px; border: 1px solid #48414f; border-radius: 10px; color: #fff8e8; font: 700 15px/1 "Courier New", monospace; background: #18161d; }
input:focus { outline: 2px solid #ff8258; outline-offset: 2px; }
.actions { display: flex; justify-content: center; flex-wrap: wrap; gap: 12px; }
button { min-width: 120px; padding: 13px 20px; border: 1px solid #48414f; border-radius: 999px; color: #d5ced9; font: inherit; font-weight: 800; background: #2b2731; cursor: pointer; }
button:hover { border-color: #746b7a; }
.primary { border-color: #ff8258; color: #17151c; background: #ff8258; }
button:focus-visible { outline: 3px solid white; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .progress { transition: none; } }
</style>
</head>
<body>
<main class="timer-card">
<span class="eyebrow">FOCUS SESSION</span><h1>Reserve um tempo de foco</h1><p class="subtitle">Defina os minutos e comece um período de trabalho sem interrupções.</p>
<div class="clock" aria-live="polite">
<svg viewBox="0 0 220 220" aria-hidden="true"><circle class="track" cx="110" cy="110" r="96"></circle><circle id="progress" class="progress" cx="110" cy="110" r="96"></circle></svg>
<div><div id="time" class="time">05:00</div><span id="state" class="state">Pronto para começar</span></div>
</div>
<div class="settings"><label for="minutes">Minutos de foco</label><input id="minutes" type="number" min="1" max="180" value="5" inputmode="numeric"></div>
<div class="actions"><button id="toggle" class="primary" type="button">Iniciar</button><button id="reset" type="button">Redefinir</button></div>
</main>
<script>
const timeDisplay = document.querySelector('#time');
const stateDisplay = document.querySelector('#state');
const progress = document.querySelector('#progress');
const minutesInput = document.querySelector('#minutes');
const toggleButton = document.querySelector('#toggle');
const resetButton = document.querySelector('#reset');
const circumference = 2 * Math.PI * 96;
let totalSeconds = 300;
let remainingSeconds = totalSeconds;
let timerId = 0;
let endAt = 0;
function render() {
const minutes = Math.floor(remainingSeconds / 60);
const seconds = remainingSeconds % 60;
timeDisplay.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
progress.style.strokeDashoffset = String(circumference * (1 - remainingSeconds / totalSeconds));
}
function stopTimer(message = 'Pausado') {
window.clearInterval(timerId);
timerId = 0;
toggleButton.textContent = remainingSeconds === 0 ? 'Reiniciar' : 'Continuar';
stateDisplay.textContent = message;
}
function tick() {
remainingSeconds = Math.max(0, Math.ceil((endAt - Date.now()) / 1000));
render();
if (remainingSeconds === 0) stopTimer('Concentração concluída · Muito bem');
}
function startTimer() {
if (remainingSeconds === 0) resetTimer();
endAt = Date.now() + remainingSeconds * 1000;
timerId = window.setInterval(tick, 250);
toggleButton.textContent = 'Pausar';
stateDisplay.textContent = 'Foco em andamento';
}
function resetTimer() {
window.clearInterval(timerId);
timerId = 0;
const minutes = Math.min(180, Math.max(1, Number(minutesInput.value) || 5));
minutesInput.value = String(minutes);
totalSeconds = minutes * 60;
remainingSeconds = totalSeconds;
toggleButton.textContent = 'Iniciar';
stateDisplay.textContent = 'Pronto para começar';
render();
}
toggleButton.addEventListener('click', () => timerId ? stopTimer() : startTimer());
resetButton.addEventListener('click', resetTimer);
minutesInput.addEventListener('change', resetTimer);
render();
</script>
</body>
</html>