WebberUI

Product Drop Countdown

A limited-release drop board with a built-in flip countdown, a CTA that unlocks at launch, and a three-state machine ending in a restock notification form once it sells out.

Loading preview…
npx shadcn@latest add https://webberui.com/r/product-drop-countdown.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

200
<ProductDropCountdown />

Installation

npx shadcn@latest add https://webberui.com/r/product-drop-countdown.json

Or, once registries are configured in components.json, install it as @webberui/product-drop-countdown.

Usage

import { ProductDropCountdown } from "@/components/ui/product-drop-countdown";

<ProductDropCountdown
  title="Aero Runner 3"
  subtitle="Limited collab colorway"
  price="NT$4,280"
  dropAt={Date.now() + 1000 * 60 * 60}
  totalStock={180}
  onPurchase={() => console.log("add to cart")}
  onNotify={(email) => console.log("restock notification", email)}
/>

The three states are derived automatically from dropAt and stock: reaching zero on the countdown enters the live state, and selling out of stock enters the sold-out state. To hand control to an outside state machine, pass a controlled state:

<ProductDropCountdown title="Aero Runner 3" dropAt={dropAt} state="soldout" />

Stock can be controlled too, driving the depletion bar directly through sold; when it is not provided, depletion is simulated automatically after launch:

<ProductDropCountdown title="Aero Runner 3" dropAt={dropAt} totalStock={200} sold={soldSoFar} />

Props

PropTypeDefaultDescription
titlestringProduct name
subtitlestringSubtitle / model number and other secondary detail
pricestringPrice display string
dropAtDate | numberRelease time; goes live automatically when the countdown reaches zero
totalStocknumber200Total stock units, driving the depletion bar and the sold-out determination
soldnumberControlled number sold; when not provided, depletion is simulated automatically after launch
state"countdown" | "live" | "soldout"Controlled three states; when provided, the automatic derivation is ignored
ctaLabelstring"Buy now"Copy on the purchase CTA
onDrop() => voidFires once on entering the live state
onSoldOut() => voidFires once on selling out
onPurchase() => voidFires when the purchase CTA is clicked
onNotify(email: string) => voidFires when the restock notification form is submitted

How it works

  • Flip clock: every digit is its own flip card; when the value changes, the front leaf falls down around its bottom edge while the back leaf flips back around its top edge, implemented with CSS 3D rotateX and updated frame by frame.
  • Launch unlock: the moment the countdown reaches zero, the locked CTA springs in and scales up on an elastic spring (stiffness: 440, damping: 12), and the stock depletion bar starts at the same time, turning amber and then red as the remaining proportion drops below 50% and 20%.
  • Sold-out desaturation: once stock hits zero, the whole section is desaturated with a grayscale filter and its opacity is lowered, and the restock notification form slides in from below; the form itself stays in full color so it remains readable and usable.

Accessibility

  • The countdown carries role="timer" and a readable aria-label (for example "1 hour 0 minutes 12 seconds until release"), while the flip digits are hidden from assistive technology (aria-hidden).
  • Remaining stock is announced through aria-live="polite", so screen readers pick up the changes in the live state.
  • The restock form's input carries a label and an aria-label and can be submitted with Enter; the CTA and the submit button both have focus-visible outlines.
  • When the user has "reduce motion" enabled at the system level, the flip switches directly, the CTA and the form fade in instead, and the desaturation completes instantly — none of which affects the three-state logic.

On this page