General

Countdown

Storybook
Live countdown to a deadline, rendering days, hours, minutes, and seconds with localized unit labels, powered by the useCountdown composable.

Loading playground

Overview

Countdown renders the time remaining until an endDate as a row of value-and-label pairs, ticking once a second. Pass a Date or an ISO string; everything else is optional.

The units shown adapt to how much time is left. Above 24 hours the row leads with days; below that it drops to hours, minutes, and seconds so the numbers stay meaningful as the deadline approaches. Once the deadline passes, the row is replaced by an expired message and the component emits expired.

Unit labels come from Intl, so they are localized and plural-aware without any translation work. The exact wording is owned by CLDR and is not customizable — unitDisplay picks how verbose it is.

Key Business & UX Benefits

  • Deadline pressure on flash sales, delivery cut-offs, and campaign endings, using the urgency cue shoppers already understand.
  • Unit labels localize themselves through Intl, so a new market ships without a translation pass for "days" and "hours".
  • One shared ticker drives every countdown on the page, so a listing full of deals costs one interval rather than one per item.
  • Server-rendered and client-rendered output agree, so a countdown above the fold does not flicker or warn on hydration.

Usage

LCountdown
<LCountdown :end-date="offerEndsAt" @expired="refreshOffer" />

Label verbosity

unitDisplay controls how the units are spelled out. The exact strings are locale-dependent:

ValueEnglish output
long (default)days · hours · minutes · seconds
shortdays · hr · min · sec
narrowd · h · m · s

Use narrow where horizontal space is tight, such as inside a product tile.

Icon

A clock icon (essentials/clock) renders ahead of the values. Pass a different iconName to swap it, or an empty string to drop it.

Pro-Tip from Larry: handle @expired rather than leaving a finished countdown on screen. Refetching the offer is usually right — a deal that has ended should stop advertising itself.

useCountdown

useCountdown is the composable behind both Countdown and CountdownBoxed. Call it directly to drive your own countdown UI. It is auto-imported.

function useCountdown(options: {
  endDate: MaybeRefOrGetter<Date | string>;
  now?: MaybeRefOrGetter<Date | string | undefined>;
  onExpired?: () => void;
}): {
  remainingMs: ComputedRef<number>;
  isExpired: ComputedRef<boolean>;
  totalSeconds: ComputedRef<number>;
  showDays: ComputedRef<boolean>;
  timeParts: ComputedRef<{ days: number; hours: number; minutes: number; seconds: number }>;
}
  • endDate — the instant to count toward. There is no start date.
  • onExpired — fires at most once, and only on the client. It covers both the tick that crosses zero and the case where the deadline had already passed at mount.
  • showDaystrue when more than 24 hours remain. When it is false, timeParts.days is 0 and the hours roll up, so an hours-only display needs no arithmetic of its own.
<script setup lang="ts">
// useCountdown is auto-imported
const { timeParts, isExpired } = useCountdown({
  endDate: () => props.endsAt,
  onExpired: () => emit('ended'),
});
</script>

The composable owns its clock through the shared useNow(1000) ticker, so every countdown on a page shares one interval. That clock is SSR-stable: the first client frame matches the server's, so no hydration anchor is needed.

A malformed endDate is treated as already expired rather than rendering NaN.

computeCountdown

For pure arithmetic with no clock and no lifecycle — unit tests, or deriving parts from a timestamp you already hold — computeCountdown(endDate, now) returns the same shape as a plain function.

Freezing the clock

Both components and the composable accept a now prop, which pins the clock to a fixed instant so the display never ticks. It exists for tests and Storybook snapshots. Leave it unset in production, where the live clock is what you want.

Feature List

  • Counts down to an `endDate` (Date or ISO string) with a one-second tick
  • Drops the days unit automatically once under 24 hours remain
  • Localized, plural-aware unit labels from Intl — no translation work per market
  • `unitDisplay` selects long, short, or narrow labels for tight layouts
  • Emits `expired` once, client-side, and swaps the values for an expired message
  • `role="timer"` with `aria-live="polite"` so assistive technology announces updates
  • One shared ticker across all countdowns on the page; SSR-stable with no hydration mismatch

API Reference

Countdown

PropDefaultType
endDaterequiredstring | Date
nowstring | Date

Freeze the clock at a fixed instant — for tests / Storybook only. Production ticks live.

iconNameessentials/clockstring
unitDisplaylong"long" | "short" | "narrow"

Verbosity of the unit labels (Intl unitDisplay):

  • 'long' — full words: days · hours · minutes · seconds
  • 'short' — abbreviations: days · hr · min · sec (English has no short form for "day", so it stays "days")
  • 'narrow' — single letters: d · h · m · s

Exact strings are locale-dependent (CLDR-owned), not customizable.

RTL i18n caveat (latent — only en/de ship today): this component renders the digit and the unit word as separate elements, which assumes the label is count-independent. That holds for 'narrow' in every locale, but NOT for 'long'/'short' in Arabic (n=1, n=2) and Hebrew (n=2): CLDR folds the count into the word and emits no numeral (e.g. ar n=2 day → "يومان" = "two days"), so the separate digit double-counts ("2 يومان").

Prescribed fix when RTL is on the roadmap (deferred, NOT yet implemented): probe per locale whether the configured style stays numeral-bearing at counts 1 and 2 — new Intl.NumberFormat(locale, { style: 'unit', unit: 'day', unitDisplay }).formatToParts(n).some(p => p.type === 'integer') — and if not, fall back to 'narrow', which is always numeral-safe. en/de keep their configured style; ar/he auto-downgrade to 'narrow'.

EventType
expired(event: "expired"): void
  • Countdown Boxed: the same clock rendered as painted digit tiles, for promotional surfaces.
  • Promotion Banner: campaign banner that renders this component when a countdown end date is set.
  • PopUp Promotion: promotional popup that can show a countdown in place of a coupon.
Copyright © 2026 Laioutr GmbH