Zoeken...  ⌘K GitHub

Countdown UI Elements

Aftel-timer tot een datum. Blokken: dagen, uren, minuten, seconden. Donkere variant.

/countdown
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