Countdown UI Elements
Aftel-timer tot een datum. Blokken: dagen, uren, minuten, seconden. Donkere variant.
src/components/ui/Countdown.astro
---
/**
* Countdown
* Aftel-timer tot een datum. Blokken: dagen, uren, minuten, seconden.
* Compact of groot formaat. Donkere variant.
*/
interface Props {
targetDate: string; /** ISO datum string: "2025-12-31T23:59:59" */
label?: string;
expiredLabel?: string;
size?: 'sm' | 'md' | 'lg';
dark?: boolean;
showSeconds?: boolean;
}
const {
targetDate,
label,
expiredLabel = 'De actie is verlopen',
size = 'md',
dark = false,
showSeconds = true,
} = Astro.props;
---
<div
class:list={['cd', `cd--${size}`, { 'cd--dark': dark }]}
data-component="countdown"
data-target={targetDate}
data-expired={expiredLabel}
>
{label && <p class="cd__label">{label}</p>}
<div class="cd__blocks">
<div class="cd__block">
<span class="cd__value" data-unit="days">--</span>
<span class="cd__unit">Dagen</span>
</div>
<span class="cd__sep">:</span>
<div class="cd__block">
<span class="cd__value" data-unit="hours">--</span>
<span class="cd__unit">Uren</span>
</div>
<span class="cd__sep">:</span>
<div class="cd__block">
<span class="cd__value" data-unit="minutes">--</span>
<span class="cd__unit">Minuten</span>
</div>
{showSeconds && (
<>
<span class="cd__sep">:</span>
<div class="cd__block">
<span class="cd__value" data-unit="seconds">--</span>
<span class="cd__unit">Seconden</span>
</div>
</>
)}
</div>
</div>
<script>
document.querySelectorAll<HTMLElement>('[data-component="countdown"]').forEach(el => {
const target = new Date(el.dataset.target!).getTime();
const expiredLabel = el.dataset.expired ?? 'Verlopen';
const valEls = {
days: el.querySelector<HTMLElement>('[data-unit="days"]'),
hours: el.querySelector<HTMLElement>('[data-unit="hours"]'),
minutes: el.querySelector<HTMLElement>('[data-unit="minutes"]'),
seconds: el.querySelector<HTMLElement>('[data-unit="seconds"]'),
};
function pad(n: number) { return String(n).padStart(2, '0'); }
function tick() {
const now = Date.now();
const diff = target - now;
if (diff <= 0) {
el.innerHTML = `<p class="cd__expired">${expiredLabel}</p>`;
return;
}
const days = Math.floor(diff / 86400000);
const hours = Math.floor((diff % 86400000) / 3600000);
const minutes = Math.floor((diff % 3600000) / 60000);
const seconds = Math.floor((diff % 60000) / 1000);
if (valEls.days) valEls.days.textContent = String(days);
if (valEls.hours) valEls.hours.textContent = pad(hours);
if (valEls.minutes) valEls.minutes.textContent = pad(minutes);
if (valEls.seconds) valEls.seconds.textContent = pad(seconds);
}
tick();
setInterval(tick, 1000);
});
</script>
<style>
.cd { display: flex; flex-direction: column; align-items: center; gap: 0.875rem; }
.cd__label {
font-size: 0.8125rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-muted, #6b7280);
}
.cd--dark .cd__label { color: rgba(255,255,255,0.5); }
.cd__blocks {
display: flex;
align-items: flex-end;
gap: 0.5rem;
}
/* Separator */
.cd__sep {
font-size: 2rem;
font-weight: 800;
color: rgba(0,0,0,0.2);
line-height: 1;
padding-bottom: 1.25rem;
letter-spacing: -0.04em;
}
.cd--sm .cd__sep { font-size: 1.25rem; padding-bottom: 0.875rem; }
.cd--lg .cd__sep { font-size: 3rem; padding-bottom: 1.625rem; }
.cd--dark .cd__sep { color: rgba(255,255,255,0.2); }
/* Block */
.cd__block {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.375rem;
min-width: 64px;
background: rgba(0,0,0,0.04);
border-radius: var(--radius, 0.5rem);
padding: 0.875rem 0.5rem 0.625rem;
}
.cd--sm .cd__block { min-width: 48px; padding: 0.625rem 0.375rem 0.5rem; }
.cd--lg .cd__block { min-width: 90px; padding: 1.25rem 0.75rem 0.875rem; }
.cd--dark .cd__block { background: rgba(255,255,255,0.08); }
/* Value */
.cd__value {
font-size: 2rem;
font-weight: 900;
letter-spacing: -0.04em;
color: var(--color-primary, #0a0a0a);
line-height: 1;
font-feature-settings: "tnum";
min-width: 2ch;
text-align: center;
}
.cd--sm .cd__value { font-size: 1.375rem; }
.cd--lg .cd__value { font-size: 3rem; }
.cd--dark .cd__value { color: #fff; }
/* Unit */
.cd__unit {
font-size: 0.625rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-muted, #6b7280);
}
.cd--sm .cd__unit { font-size: 0.5625rem; }
.cd--lg .cd__unit { font-size: 0.75rem; }
.cd--dark .cd__unit { color: rgba(255,255,255,0.4); }
/* Expired */
.cd__expired {
font-size: 1rem;
font-weight: 600;
color: var(--color-muted, #6b7280);
padding: 1.5rem;
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
targetDate * | string | — | ISO datum string: "2025-12-31T23:59:59" |
label | string | — | Label boven de timer |
expiredLabel | string | 'De actie is verlopen' | Tekst als timer op nul staat |
size | 'sm' | 'md' | 'lg' | 'md' | Grootte |
dark | boolean | false | Donkere variant |
showSeconds | boolean | true | Toon seconden blok |
* = verplicht