webdevpuneet.com

Circular Countdown — SVG Ring Countdown Timer JS

Circular Countdown · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

Copy & paste

Source Code

<div class="cc-stage">
  <div class="cc-ring">
    <svg viewBox="0 0 120 120" class="cc-svg">
      <circle class="cc-track" cx="60" cy="60" r="54"/>
      <circle class="cc-prog" id="ccProg" cx="60" cy="60" r="54"/>
    </svg>
    <div class="cc-num" id="ccNum">10</div>
  </div>
  <div class="cc-controls">
    <button type="button" class="cc-btn" id="ccStart">Start</button>
    <button type="button" class="cc-btn cc-ghost" id="ccReset">Reset</button>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

SVG ring via dashoffset
One stroked circle with stroke-dasharray = circumference, emptied by animating dashoffset.
Depletes from the top
The SVG is rotated −90° so the ring drains from twelve o'clock.
Drift-free timing
Remaining time is computed from a target timestamp each frame, so it never accumulates error.
requestAnimationFrame loop
Smooth depletion without a high-frequency interval; auto-pauses in background tabs.
Threshold colours
The ring shifts to amber under 50% and red under 20% for at-a-glance urgency.
Synced ring and number
Both derive from the same remaining fraction, so they never disagree.
Start / reset / done states
Start disables while running; Reset restores; zero shows a Done state.
One-constant duration
Change DURATION to repurpose for any countdown length — no library.

About this UI Snippet

Circular Countdown — A Depleting SVG Ring Timer with Threshold Colours

Screenshot of the Circular Countdown snippet rendered live

A circular countdown — a ring that empties as the seconds tick down, with the number in the centre — is the timer you see on quizzes, OTP screens, auctions, and "your session expires in…" prompts. This snippet builds it in plain HTML, CSS, SVG, and vanilla JavaScript: a stroke-dashoffset ring synced to a time-accurate countdown, colour shifts as time runs low, and start/reset controls — no library.

The ring is one stroked circle

The progress arc is an SVG circle whose stroke-dasharray is set to its full circumference (2πr), so the entire stroke is one dash exactly as long as the ring. Animating stroke-dashoffset from 0 toward that circumference progressively hides the stroke, emptying the ring. The whole SVG is rotated −90° so the ring depletes from the top, the orientation people expect. This dash-offset technique is the standard, dependency-free way to draw any circular progress or timer.

Time-based, not tick-counting

The countdown computes remaining from a target end timestamp (endAt − Date.now()) on every animation frame, rather than subtracting a fixed amount each tick. This is the crucial correctness detail: a setInterval(…, 1000) that decrements a counter drifts whenever the tab is throttled or a tick is late, so a "10-second" timer can take 11+ seconds. Deriving the remaining time from the wall clock means the timer is always accurate to real elapsed time and self-corrects after any stall — and the ring, driven by the same fraction, stays perfectly in sync with the number.

Smooth ring, honest number

The ring uses a CSS transition on stroke-dashoffset, so between frames it glides continuously rather than stepping, while the centre number shows whole seconds via Math.ceil. Driving the visual with requestAnimationFrame keeps the depletion buttery without a high-frequency interval, and pauses automatically when the tab is hidden (rAF doesn't run in background tabs), then catches up correctly because the time is recomputed from the clock.

Threshold colour shifts

As the remaining fraction crosses 50% and 20%, the ring shifts from its base colour to amber and then red, giving an at-a-glance sense of urgency without reading the number. The thresholds are simple fraction checks toggling classes, with a colour transition so the change eases in rather than snapping.

Start, reset, and a done state

Start kicks off (or resumes from a reset), disabling itself while running; Reset cancels the frame loop and restores the full ring; and on reaching zero the centre swaps to a "Done" state. Because everything derives from DURATION and the end timestamp, you can repurpose it for any length — a 30-second quiz question, a 60-second OTP window, a 5-minute break timer — by changing one constant. It's a complete reference for SVG ring progress and drift-free countdown timing.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA circular countdown renders at 10 with a full ring and Start/Reset buttons.
  2. 2
    Start itClick Start — the ring depletes from the top in sync with the centre number.
  3. 3
    Watch the colour shiftThe ring turns amber under 50% and red under 20% of the time remaining.
  4. 4
    Reset anytimeClick Reset to cancel and restore the full ring and starting number.
  5. 5
    Change the durationEdit the DURATION constant for any countdown length.
  6. 6
    Hook the done stateRun your action where the timer reaches zero (the "Done" branch).

Real-world uses

Common Use Cases

Quiz and exam timers
Show time left per question beside a quiz card, with the ring depleting from the top as an SVG rotated minus 90 degrees.
OTP and session expiry
Visualise a code's validity window next to an OTP input, changing colour as the final seconds approach.
Auction and flash sale urgency
Pair with a countdown timer so a ring and numeric clock reinforce each other, with drift-free timing from a target timestamp.
Focus and break timers
Complement a pomodoro timer with a ring that empties smoothly using requestAnimationFrame rather than a fast interval.
Resend cooldown indicators
Show a cooldown ring before enabling Resend code, as a clear signal of when a user may try again, and see SVG progress ring for the filling version.
Related: 3D Rotating Cube Loader
See the 3D Rotating Cube Loader for a related loaders pattern worth pairing with this one.
Related: Skeleton Shape Morph Reveal
See the Skeleton Shape Morph Reveal for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The progress arc is an SVG circle whose stroke-dasharray equals its full circumference (2πr), making the stroke one dash as long as the ring. Increasing stroke-dashoffset toward that circumference progressively hides the stroke, so the ring drains. The SVG is rotated −90° so it empties from the top, and a CSS transition on dashoffset keeps the motion smooth between updates.

A setInterval that subtracts 1 each second drifts: if the tab is throttled or a tick fires late, the timer runs long. Computing remaining = (endAt − Date.now()) / 1000 every frame derives the time from the wall clock, so it's always accurate to real elapsed time and self-corrects after any stall. The ring uses the same value, so visual and number stay in sync.

requestAnimationFrame pauses in hidden tabs, so the loop stops drawing — but because the remaining time is recomputed from the end timestamp, when the tab becomes visible again the timer immediately reflects the correct elapsed time and the ring jumps to the right position. There's no drift and no need for a separate background timer.

Edit the DURATION constant (in seconds). Everything else — the starting number, the ring fraction, the threshold colour points, and the end timestamp — derives from it, so a single change repurposes the timer for a 30-second question, a 60-second OTP window, or a 5-minute break. The circumference constant only depends on the radius, which you'd change in both the CSS and the 2πr calc if resizing the ring.

In React, store remaining in state, run the rAF loop in a useEffect (cancelling on cleanup), and bind the ring's strokeDashoffset and the number; in Vue, use a ref with onMounted/onUnmounted; in Angular, use ngAfterViewInit/ngOnDestroy. The timestamp-based timing and dashoffset math are framework-agnostic — only the loop lifecycle and bindings move into the framework.