Countdown
<x-wirekit:: shows the time remaining until an absolute deadline,
ticking down every second on the client — no wire:poll, no server round-trips.
It colors itself when the deadline is close (urgent) and again once it has passed
(overdue), so a running deadline reads at a glance instead of forcing the reader
to subtract a timestamp from the current time in their head.
Pass an absolute instant (a Carbon, an ISO-8601 string, or a unix timestamp) — never a duration. A duration drifts the moment the tab sleeps or the page is cached; an absolute instant is recomputed from the real clock on every tick.
Usage
The largest meaningful unit leads and shrinks down to the smallest — years
appear only once the remaining time reaches a year, so a far-off deadline reads
in years and days instead of tens of thousands of days. A year counts as 365
days, so a deadline years away shows one day more for each leap day in between;
leave years out of units when the count has to match the
calendar:
Urgent threshold
Set warn-threshold (in seconds) to switch the countdown to its urgent color
before the deadline slips — here, the last hour:
Overdue
Once the deadline has passed, the countdown shows its overdue text in the danger
color. Provide your own copy with expired-text:
Without seconds
For a long deadline, drop the churning seconds with :show-seconds="false":
Choose your units
Pass an explicit ordered subset to units — any combination of years, days,
hours, minutes, seconds. The largest unit you list carries all the overflow
above it (so units="hours minutes" shows the total hours), and the smallest
truncates. Large values get locale-aware thousands separators (like
price); pass locale to override, or :separators="false"
to turn them off:
Segments variant
variant="segments" renders each unit as its own boxed block with a label — the
classic dashboard countdown look. When a value changes, it animates (the default
box pulse — see Change animation for the styles):
Change animation
The segments variant animates each unit as its value changes, in one of two
styles set with animate. It plays for the first seconds after the countdown
appears, most visibly on the seconds box, and then stops while the value keeps
ticking, so the countdown never blinks for longer than the five seconds
WCAG 2.2.2
allows without a control to stop it. Both styles honor prefers-reduced-motion
(an instant, motionless update for users who ask for less motion).
animate="box" (the default) pulses the whole box — border and background
flash the accent color with a small scale pop:
animate="text" flashes only the changing number's color (to your theme's
accent) — the box stays still. It is the understated alternative to the box
pulse; it reads strongest on a theme with a colored accent:
Turn motion off entirely with :animate="false":
Livewire
The countdown ticks entirely on the client, so it never triggers a Livewire round-trip. Bind the deadline itself as a normal prop:
<x-wirekit::countdown :until="$order->payment_due_at" :warn-threshold="600" />
Completion events. The countdown is more than display — it notifies when the deadline passes, so a sibling control can react (re-enable a button, refetch, show a retry form):
wirekit-countdown-expired— a bubbling event dispatched once the moment the deadline is reached (or immediately for an already-past deadline).x-modelable="done"— a writable boolean that flips totrueat expiry, so you can observe completion withx-model.
{{-- Re-enable "Resend code" only after the retry window elapses --}}
<div x-data="{ ready: false }">
<x-wirekit::countdown :until="now()->addSeconds($retryAfter)" x-model="ready" />
<x-wirekit::button x-bind:disabled="!ready" wire:click="resend">Resend code</x-wirekit::button>
</div>
The retry-timer pattern pairs naturally with a rate limiter — feed it
RateLimiter:: via :until="now()->add.
Headless: your own copy around the number
Sometimes you want the countdown's clock — the client tick, the deadline recompute on wake, the completion event — but your own wording around the number ("Resend available in 45 seconds", with your locale's pluralization). Pass a slot and the countdown becomes headless: it renders your markup instead of the default units, and your Alpine expressions resolve against its live state.
<x-wirekit::countdown :until="now()->addSeconds($retryAfter)">
{{-- `remaining` is the full breakdown plus totals; `expired` / `urgent` /
`done` are the same states the default display uses. --}}
<span x-show="!expired" x-text="`Resend available in ${remaining.totalSeconds} seconds`"></span>
<span x-show="expired">You can resend now.</span>
</x-wirekit::countdown>
The reactive state available inside the slot:
| Expression | What it gives you |
|---|---|
remaining |
{ years, days, hours, minutes, seconds, totalSeconds, totalMs } — the complete canonical ladder plus totals, independent of the units / variant props. |
expired |
true once the deadline has passed. |
urgent |
true inside the warn-threshold window. |
done |
The writable completion boolean (also x-modelable). |
In headless mode you own the accessible text too — the default screen-reader
<time> is not rendered, so add your own (for example a one-time role="status"
announcement at zero) to match your app's pattern.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
until |
DateTimeInterface|string|int |
null |
The absolute target instant: a date object of any class (Carbon, CarbonImmutable, DateTime), an ISO-8601 string or a Unix timestamp. A date object counts to the instant it holds in its own zone, so a model attribute counts the same whether your application keeps the default dates or has called Date::use(CarbonImmutable::class). A null value renders as immediately overdue. |
expiredText |
string|null |
null |
Text shown once the deadline has passed. Defaults to a translatable "Overdue". |
warnThreshold |
int|null |
null |
Seconds-remaining threshold below which the countdown turns urgent (warning color). Omit for no urgent state. |
units |
string |
'auto' |
'auto' shows years→seconds and drops leading zero-units, or an explicit ordered subset like "days hours minutes". The largest listed unit carries the overflow. A year counts as 365 days. |
showSeconds |
bool |
true |
Include seconds in 'auto' mode (ignored when units is an explicit list). |
variant |
string |
'inline' |
'inline' (e.g. 73y 190d 12h) or 'segments' (boxed blocks with labels). |
separators |
bool |
true |
Locale-aware thousands separators on large unit values. |
locale |
string|null |
null |
BCP-47 locale for the separators. Defaults to the app locale. |
timezone |
string|null |
null |
The zone the deadline is written in for the reader, such as Europe/Berlin: the deadline in the accessible name and the machine-readable time. Defaults to the zone of the value. The countdown counts to the same instant either way. |
animate |
bool|string |
true |
Change animation. For segments: "box" (default — the whole box pulses with a border + accent flash + scale pop) or "text" (only the changing number flashes color). false / "none" turns it off. It plays for the first seconds after the countdown appears, then stops. The inline variant changes its value without animation. Honors prefers-reduced-motion. |
scope |
string|null |
null |
Scoped personalization key. |
Accessibility
- The root carries
role="timer", which is a live region set to off — so the ticking value is not announced every second. - The accessible name is the absolute deadline (
aria-label), a stable value a screen reader reads on navigation. - A visually hidden
<time datetime>holds a coarse "time remaining" phrase in the active units (e.g. "2 days, 5 hours"), so assistive tech can read the current remaining time on demand without per-second spam. - The visible ticking units are
aria-hidden— a decorative convenience over the semantic time. - The change animation of the
segmentsvariant stops a few seconds after the countdown appears. The value itself keeps updating: where the countdown stands beside other content and the seconds are not essential to the task, a unit list withoutsecondschanges once a minute instead of every second.
Keyboard Interaction
This component is purely presentational and does not respond to keyboard input.