Countdown
Fri 30 Oct · 09:00
import { useEffect, useState } from 'react';
const css = `
.card-countdown {
width: 246px;
padding: 14px;
border-radius: 16px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
}
.card-countdown__head { display: flex; justify-content: space-between; align-items: center; }
.card-countdown__head b { color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-countdown__head span { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.card-countdown__ring { width: 22px; height: 22px; transform: rotate(-90deg); }
.card-countdown__ring circle { fill: none; stroke-width: 3; }
.card-countdown__ring circle:first-child { stroke: light-dark(#ebedf0, #232529); }
.card-countdown__ring circle:last-child { stroke: light-dark(#373a40, #f1f2f3); stroke-linecap: round; stroke-dasharray: 1; transition: stroke-dashoffset 0.9s linear; }
.card-countdown__units { display: flex; gap: 6px; margin-top: 14px; }
.card-countdown__unit { flex: 1; display: grid; justify-items: center; gap: 4px; }
.card-countdown__unit small { color: light-dark(#5f626a, #a7abb2); font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase; }
.card-countdown__digits { display: flex; gap: 2px; }
.card-countdown__reel {
position: relative;
width: 20px;
height: 32px;
overflow: hidden;
border-radius: 6px;
background: light-dark(#fbfcfd, #121417);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
}
.card-countdown__strip { display: flex; flex-direction: column; transition: transform 0.45s cubic-bezier(0.34, 1.3, 0.64, 1); }
.card-countdown__strip span { display: grid; place-items: center; height: 32px; color: light-dark(#373a40, #f1f2f3); font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) { .card-countdown__strip, .card-countdown__ring circle:last-child { transition: none; } }
`;
const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const START = 3 * 86400 + 14 * 3600 + 7 * 60 + 52; // 3d 14h 07m 52s
function Reel({ d }: { d: number }) {
return (
<span className="card-countdown__reel">
<span className="card-countdown__strip" style={{ transform: `translateY(${-d * 32}px)` }}>
{DIGITS.map((n) => (
<span key={n}>{n}</span>
))}
</span>
</span>
);
}
export function CountdownCard() {
const [left, setLeft] = useState(START);
useEffect(() => {
const id = window.setInterval(() => setLeft((s) => (s > 0 ? s - 1 : START)), 1000);
return () => clearInterval(id);
}, []);
const units = [
['Days', Math.floor(left / 86400)],
['Hrs', Math.floor(left / 3600) % 24],
['Min', Math.floor(left / 60) % 60],
['Sec', left % 60],
] as const;
return (
<>
<style>
{css}
</style>
<div className="card-countdown" role="timer" aria-label={`Launch in ${units.map(([k, v]) => `${v} ${k}`).join(', ')}`}>
<div className="card-countdown__head">
<div>
<b>Launch day</b>
<br />
<span>Fri 30 Oct · 09:00</span>
</div>
<svg className="card-countdown__ring" viewBox="0 0 22 22" aria-hidden="true">
<circle cx={11} cy={11} r={8} />
<circle cx={11} cy={11} r={8} pathLength={1} style={{ strokeDashoffset: 1 - (left % 60) / 60 }} />
</svg>
</div>
<div className="card-countdown__units" aria-hidden="true">
{units.map(([k, v]) => (
<div key={k} className="card-countdown__unit">
<span className="card-countdown__digits">
<Reel d={Math.floor(v / 10)} />
<Reel d={v % 10} />
</span>
<small>{k}</small>
</div>
))}
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardCountdown(el) on the mounted element instead.
<div class="card-countdown" role="timer" aria-label="Launch in 3 Days, 14 Hrs, 7 Min, 52 Sec">
<div class="card-countdown__head">
<div>
<b>Launch day</b>
<br/>
<span>Fri 30 Oct · 09:00</span>
</div>
<svg class="card-countdown__ring" viewBox="0 0 22 22" aria-hidden="true">
<circle cx="11" cy="11" r="8"></circle>
<circle cx="11" cy="11" r="8" pathLength="1" style="stroke-dashoffset:0.1333333333333333"></circle>
</svg>
</div>
<div class="card-countdown__units" aria-hidden="true">
<div class="card-countdown__unit">
<span class="card-countdown__digits">
<span class="card-countdown__reel">
<span class="card-countdown__strip" style="transform:translateY(0px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
</span>
<span class="card-countdown__reel">
<span class="card-countdown__strip" style="transform:translateY(-96px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
</span>
</span>
<small>Days</small>
</div>
<div class="card-countdown__unit">
<span class="card-countdown__digits">
<span class="card-countdown__reel">
<span class="card-countdown__strip" style="transform:translateY(-32px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
</span>
<span class="card-countdown__reel">
<span class="card-countdown__strip" style="transform:translateY(-128px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
</span>
</span>
<small>Hrs</small>
</div>
<div class="card-countdown__unit">
<span class="card-countdown__digits">
<span class="card-countdown__reel">
<span class="card-countdown__strip" style="transform:translateY(0px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
</span>
<span class="card-countdown__reel">
<span class="card-countdown__strip" style="transform:translateY(-224px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
</span>
</span>
<small>Min</small>
</div>
<div class="card-countdown__unit">
<span class="card-countdown__digits">
<span class="card-countdown__reel">
<span class="card-countdown__strip" style="transform:translateY(-160px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
</span>
<span class="card-countdown__reel">
<span class="card-countdown__strip" style="transform:translateY(-64px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
</span>
</span>
<small>Sec</small>
</div>
</div>
</div>.card-countdown {
width: 246px;
padding: 14px;
border-radius: 16px;
background: light-dark(#f3f4f7, #1a1c1f);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
}
.card-countdown__head { display: flex; justify-content: space-between; align-items: center; }
.card-countdown__head b { color: light-dark(#373a40, #f1f2f3); font-size: 13px; font-weight: 600; }
.card-countdown__head span { color: light-dark(#5f626a, #a7abb2); font-size: 11px; }
.card-countdown__ring { width: 22px; height: 22px; transform: rotate(-90deg); }
.card-countdown__ring circle { fill: none; stroke-width: 3; }
.card-countdown__ring circle:first-child { stroke: light-dark(#ebedf0, #232529); }
.card-countdown__ring circle:last-child { stroke: light-dark(#373a40, #f1f2f3); stroke-linecap: round; stroke-dasharray: 1; transition: stroke-dashoffset 0.9s linear; }
.card-countdown__units { display: flex; gap: 6px; margin-top: 14px; }
.card-countdown__unit { flex: 1; display: grid; justify-items: center; gap: 4px; }
.card-countdown__unit small { color: light-dark(#5f626a, #a7abb2); font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase; }
.card-countdown__digits { display: flex; gap: 2px; }
.card-countdown__reel {
position: relative;
width: 20px;
height: 32px;
overflow: hidden;
border-radius: 6px;
background: light-dark(#fbfcfd, #121417);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
}
.card-countdown__strip { display: flex; flex-direction: column; transition: transform 0.45s cubic-bezier(0.34, 1.3, 0.64, 1); }
.card-countdown__strip span { display: grid; place-items: center; height: 32px; color: light-dark(#373a40, #f1f2f3); font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; }
@media (prefers-reduced-motion: reduce) { .card-countdown__strip, .card-countdown__ring circle:last-child { transition: none; } }function initCardCountdown(root) {
var strips = root.querySelectorAll('.card-countdown__strip');
var ring = root.querySelector('.card-countdown__ring circle:last-child');
if (strips.length !== 8 || !ring) return;
var START = 3 * 86400 + 14 * 3600 + 7 * 60 + 52; // 3d 14h 07m 52s
var NAMES = ['Days', 'Hrs', 'Min', 'Sec'];
var left = START;
function render() {
var units = [Math.floor(left / 86400), Math.floor(left / 3600) % 24, Math.floor(left / 60) % 60, left % 60];
units.forEach(function (v, i) {
strips[i * 2].style.transform = 'translateY(' + -Math.floor(v / 10) * 32 + 'px)';
strips[i * 2 + 1].style.transform = 'translateY(' + -(v % 10) * 32 + 'px)';
});
ring.style.strokeDashoffset = String(1 - (left % 60) / 60);
root.setAttribute('aria-label', 'Launch in ' + units.map(function (v, i) { return v + ' ' + NAMES[i]; }).join(', '));
}
var timer = setInterval(function () {
left = left > 0 ? left - 1 : START;
render();
}, 1000);
return function () { clearInterval(timer); };
}
document.querySelectorAll('.card-countdown').forEach(initCardCountdown);Goal: Create a "Countdown" card.
Visual behavior: A launch countdown: days, hours, minutes and seconds on rolling digit reels that spring to each new value, with a small ring draining once a minute in the corner.
Technique:
- Each unit is two reels; a reel is a 32px window over a strip of the digits 0 to 9, translated to the current digit with a slightly overshooting transition, so changes roll.
- An interval subtracts one second; days, hours, minutes and seconds are derived from the total.
- The corner ring is an SVG circle with pathLength 1 whose dashoffset tracks the seconds in the current minute.
Accessibility: role="timer" with an aria-label that spells out the time left; the reels are aria-hidden. Under prefers-reduced-motion the digits change without rolling.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Countdown" card.
Visual behavior: A launch countdown: days, hours, minutes and seconds on rolling digit reels that spring to each new value, with a small ring draining once a minute in the corner.
Technique:
- Each unit is two reels; a reel is a 32px window over a strip of the digits 0 to 9, translated to the current digit with a slightly overshooting transition, so changes roll.
- An interval subtracts one second; days, hours, minutes and seconds are derived from the total.
- The corner ring is an SVG circle with pathLength 1 whose dashoffset tracks the seconds in the current minute.
Accessibility: role="timer" with an aria-label that spells out the time left; the reels are aria-hidden. Under prefers-reduced-motion the digits change without rolling.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A launch countdown: days, hours, minutes and seconds on rolling digit reels that spring to each new value, with a small ring draining once a minute in the corner.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, spring, card
- Techniques
- interval, css transition, stroke dashoffset
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
role="timer" with an aria-label that spells out the time left; the reels are aria-hidden. Under prefers-reduced-motion the digits change without rolling.