CSS Countdown Animation: Numbers and Ring Timers

A pure CSS countdown animates a registered integer custom property from the start number to zero and prints it with a counter. Register --n with @property as an integer, animate it with steps() so it ticks once a second, and show it with counter-reset: n var(--n) and content: counter(n). A ring adds an animated stroke-dashoffset.

The parts of a CSS-only countdown
PieceCSSJob
Typed variable@property --n { syntax: "<integer>" }Lets the browser animate the number
Tickinganimation: count 3s steps(3) forwardsOne jump per second, holds the last value
Printingcounter-reset: n var(--n)Turns the variable into a counter
Showing::after { content: counter(n) }Draws the current number
Ringstroke-dashoffset: 0 to circumferenceEmpties a circle over the same time

Updated

A splash countdown, a quiz timer or a launch moment does not need a script. This page builds a 3-2-1 number countdown and a ring countdown with CSS alone, lets you generate one with your own start number and speed, and gives a fallback for older browsers. For a countdown to a calendar date, use our countdown timer generator instead.

Countdown generator

<style>
  @property --n { syntax: "<integer>"; inherits: false; initial-value: 0; }
  .countdown { position: relative; width: 120px; height: 120px; display: grid; place-items: center;
               font: 700 48px system-ui; color: #0f172a }
  .countdown .num { animation: count 3s steps(3) forwards; counter-reset: n var(--n) }
  .countdown .num::after { content: counter(n) }
  @keyframes count { from { --n: 3 } to { --n: 0 } }
  .ring { position: absolute; inset: 0; transform: rotate(-90deg) }
  .ring circle { fill: none; stroke-width: 8 }
  .ring .track { stroke: #e2e8f0 }
  .ring .bar { stroke: #4F98FB; stroke-linecap: round; stroke-dasharray: 283;
               animation: empty 3s linear forwards }
  @keyframes empty { to { stroke-dashoffset: 283 } }
  @media (prefers-reduced-motion: reduce) { .ring .bar { animation: none } }
</style>
<div class="countdown" role="timer" aria-label="Countdown from 3">
  <svg class="ring" viewBox="0 0 100 100" aria-hidden="true"><circle class="track" cx="50" cy="50" r="45"/><circle class="bar" cx="50" cy="50" r="45"/></svg>
  <span class="num"></span>
</div>

3-2-1 number countdown

Ticking number with @property and a counter

Edit the code to rerun it. The number drops once a second and stays at 0.

<style>
  @property --n { syntax: "<integer>"; inherits: false; initial-value: 0; }
  .countdown { position: relative; width: 120px; height: 120px; display: grid; place-items: center;
               font: 700 48px system-ui; color: #0f172a }
  .countdown .num { animation: count 3s steps(3) forwards; counter-reset: n var(--n) }
  .countdown .num::after { content: counter(n) }
  @keyframes count { from { --n: 3 } to { --n: 0 } }
</style>
<div class="countdown" role="timer" aria-label="Countdown from 3">
  <span class="num"></span>
</div>

Ring countdown

Number inside an emptying ring

The ring uses stroke-dashoffset on an SVG circle and stops moving when reduced motion is requested.

<style>
  @property --n { syntax: "<integer>"; inherits: false; initial-value: 0; }
  .countdown { position: relative; width: 120px; height: 120px; display: grid; place-items: center;
               font: 700 48px system-ui; color: #0f172a }
  .countdown .num { animation: count 5s steps(5) forwards; counter-reset: n var(--n) }
  .countdown .num::after { content: counter(n) }
  @keyframes count { from { --n: 5 } to { --n: 0 } }
  .ring { position: absolute; inset: 0; transform: rotate(-90deg) }
  .ring circle { fill: none; stroke-width: 8 }
  .ring .track { stroke: #e2e8f0 }
  .ring .bar { stroke: #4F98FB; stroke-linecap: round; stroke-dasharray: 283;
               animation: empty 5s linear forwards }
  @keyframes empty { to { stroke-dashoffset: 283 } }
  @media (prefers-reduced-motion: reduce) { .ring .bar { animation: none } }
</style>
<div class="countdown" role="timer" aria-label="Countdown from 5">
  <svg class="ring" viewBox="0 0 100 100" aria-hidden="true"><circle class="track" cx="50" cy="50" r="45"/><circle class="bar" cx="50" cy="50" r="45"/></svg>
  <span class="num"></span>
</div>

Fallback without @property

Stacked numbers with animation-delay

Each number is its own element and is shown for one second in turn. It works in any browser that supports CSS animations.

<style>
  .stack { position: relative; width: 80px; height: 64px; font: 700 48px system-ui; text-align: center }
  .stack span { position: absolute; inset: 0; opacity: 0; animation: show 1s steps(1) forwards }
  .stack span:nth-child(2) { animation-delay: 1s }
  .stack span:nth-child(3) { animation-name: stay; animation-delay: 2s }
  @keyframes show { from { opacity: 1 } to { opacity: 0 } }
  @keyframes stay { from, to { opacity: 1 } }
</style>
<div class="stack" role="timer"><span>3</span><span>2</span><span>1</span></div>

Why the counter trick works

Registering --n as an integer gives it a type, so keyframes from 3 to 0 produce in-between whole numbers instead of a sudden swap. counter-reset accepts that integer through var(), and counter() turns it into text in a pseudo-element.

steps(3) splits a three second animation into three equal jumps. Without it you get the same numbers, but timing follows the easing curve and the ticks feel uneven.

Drawing the ring

An SVG circle's outline can be drawn as one dash as long as the circumference, 2 times pi times the radius. Moving stroke-dashoffset from zero to that length slides the dash out of view, so the ring empties smoothly.

Rotate the SVG by minus 90 degrees so the ring starts emptying from the top, and set stroke-linecap: round for a softer end.

How it works

  1. Pick the start number and seconds per step in the generator.
  2. Choose the number style, the ring style or both, and press replay to watch it again.
  3. Copy the HTML and CSS. It runs with no JavaScript at all.
  4. Read the fallback section if you must support browsers without @property.

Tips

Frequently asked questions

Why does my countdown jump straight to zero?

The custom property is not registered. An unregistered custom property cannot be interpolated, so the browser flips from the first value to the last. Add an @property rule with syntax "<integer>", inherits and an initial-value.

Which browsers support @property?

MDN lists @property as Baseline 2024, newly available across current browsers since July 2024. In older browsers the number does not tick, which is why a stacked-number fallback is shown on this page.

How do I make the countdown show Go at the end?

Add a second element with the word, start it at opacity 0 and give it an animation that fades in with an animation-delay equal to the countdown length. Hide the number at the same moment.

Is this the same as a countdown to a date?

No. A date countdown needs the current time, which CSS cannot read, so it requires JavaScript. Our countdown timer generator builds that kind. This page is for short fixed countdowns like 3-2-1.

Related: Countdown timer generator (to a date), CSS animation generator, CSS variables guide, CSS transitions playground, prefers-reduced-motion guide.

Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.

More in Guides

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap