Saltar al contenido principal

<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:

NameTypeRequiredDefaultDescription
asChildbooleanNofalseSwap the root element via Radix Slot.
childrenReactNode | ((args: { secondsRemaining, isExpired, formatted }) => ReactNode)NoReplace the default rendering. Function form receives the live state.
now() => DateNoWall-clock source. Forwarded to useReservationCountdown. Use for tests.
emptyFallbackReactNodeNonullRendered when there's no active reservation.
classNamestringNoAppended to the SDK's rift-countdown class.
…restHTMLAttributes<HTMLSpanElement>NoForwarded 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 derives secondsRemaining and isExpired from the active reservation's expiresAt and 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 strings prop on <RiftProvider> to wrap the value.
  • Empty/no-reservation state. Returns emptyFallback (default null). 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")} />