<Countdown>
Per-second MM:SS countdown to the active reservation's expiresAt.
Renders nothing when there is no active reservation. Supports a
function-children render prop for custom display, and a now
injection point for tests / Storybook.
Import
import { Countdown } from "@feelrift/react";
Basic usage
import { Countdown, RiftEvent, RiftProvider } from "@feelrift/react";
<RiftProvider>
<RiftEvent eventId="evt_summerfest_2026">
<Countdown />
</RiftEvent>
</RiftProvider>;
Props
Accepts all HTMLAttributes<HTMLSpanElement> plus:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
asChild | boolean | No | false | Swap the root element via Radix Slot. |
children | ReactNode | ((args: { secondsRemaining, isExpired, formatted }) => ReactNode) | No | — | Replace the default rendering. Function form receives the live state. |
now | () => Date | No | — | Wall-clock source. Forwarded to useReservationCountdown. Use for tests. |
emptyFallback | ReactNode | No | null | Rendered when there's no active reservation. |
className | string | No | — | Appended to the SDK's rift-countdown class. |
…rest | HTMLAttributes<HTMLSpanElement> | No | — | Forwarded to the root element. |
children
Two forms:
- JSX child — replaces the default MM:SS text inside the wrapper span.
- Function child — receives
{ secondsRemaining, isExpired, formatted }and returns whatever you want. Use for circular progress rings, custom expired states, or non-MM:SS displays.
now
Wall-clock source override. Defaults to Date.now; pass a stub for
Storybook scenarios or unit tests where time needs to be controlled.
Render output
Renders <span class="rift-countdown">, or
<span class="rift-countdown rift-countdown--expired"> once
secondsRemaining reaches zero. Default text:
- While ticking:
MM:SS(e.g.09:42). - After expiry:
t("reservation.expired").
When there's no active reservation, renders emptyFallback
(null by default).
Behavior
- Data source. Reads from
useReservationCountdown(), which derivessecondsRemainingandisExpiredfrom the active reservation'sexpiresAtand re-renders every second. - Stops at 00:00. The component doesn't continue past zero —
the SDK doesn't poll after expiry. A checkout already in flight
may still complete briefly past
expiresAt, but the UI must not depend on this. - No label baked in. The default rendering is bare MM:SS — no
"expires in" prefix. Many designs put the label in a sibling
element; if you want it inline, use a function-children form or
the
stringsprop on<RiftProvider>to wrap the value. - Empty/no-reservation state. Returns
emptyFallback(defaultnull). The most permissive default for layouts that don't expect a placeholder. - Reservation expiry behavior. When the countdown reaches zero,
<RiftProvider on={{ reservationExpired }}>fires. The hook does NOT auto-clear the reservation; that's the consumer's call.
Examples
Inline label
<p>
Expires in <Countdown />
</p>
Custom expired state via function child
<Countdown>
{({ secondsRemaining, isExpired, formatted }) =>
isExpired ? (
<span className="text-error">Time's up — pick again</span>
) : (
<span aria-live="polite">{formatted} remaining</span>
)
}
</Countdown>
Circular progress ring (live state via render prop)
<Countdown>
{({ secondsRemaining }) => (
<CircularProgress
value={secondsRemaining}
max={600 /* 10-minute reservation */}
/>
)}
</Countdown>
Slot the root for ARIA semantics
<Countdown asChild>
<output aria-live="polite" />
</Countdown>
Stubbed clock for Storybook
<Countdown now={() => new Date("2026-09-01T12:00:00Z")} />
Related
useReservationCountdown— underlying hook. Public types exported from@feelrift/react.<ReservationSummary>— usually rendered alongside.<RiftProvider>—on.reservationExpired— fires when the countdown reaches zero.