Flash Sale Offer Bar
blockecommerceLimited-time flash sale countdown deal bar with stock claim progress meter, tiered multi-buy volume discounts, and 1-click cart activation.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/flash-sale-offer-bar.json$npx shadcn@latest add https://uipkge.dev/r/react/flash-sale-offer-bar.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/flash-sale-offer-bar.json$bunx shadcn@latest add https://uipkge.dev/r/react/flash-sale-offer-bar.jsonNamed registry:
npx shadcn@latest add @uipkge-react/flash-sale-offer-barInstalls to:components/blocks/flash-sale-offer-bar/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
saleTitle | string | — | optional |
endTimeIso | string | — | optional |
claimedPercent | number | — | optional |
itemsLeft | number | — | optional |
currentTier | number | — | optional |
tiers | TierOffer[] | — | optional |
onSelectTier | (tier: TierOffer) => void | — | optional |
onClaimDeal | () => void | — | optional |
className | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
TierOfferinterface TierOffer {
id: string
quantity: number
discountPct: number
label: string
badgeText?: string
popular?: boolean
}npm dependencies
Files installed (2)
components/blocks/flash-sale-offer-bar/FlashSaleOfferBar.tsx8 kB
'use client' import * as React from 'react' import { Flame, ShieldCheck, Tag } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' export interface TierOffer { id: string quantity: number discountPct: number label: string badgeText?: string popular?: boolean } export interface FlashSaleProps { saleTitle?: string endTimeIso?: string claimedPercent?: number itemsLeft?: number currentTier?: number tiers?: TierOffer[] onSelectTier?: (tier: TierOffer) => void onClaimDeal?: () => void className?: string } const defaultTiers: TierOffer[] = [ { id: 't1', quantity: 1, discountPct: 15, label: 'Standard Pack', badgeText: 'Save $45' }, { id: 't2', quantity: 2, discountPct: 25, label: 'Duo Studio Bundle', badgeText: 'Save $150 · Best Value', popular: true, }, { id: 't3', quantity: 3, discountPct: 35, label: 'Team / Lab Triad', badgeText: 'Save $315' }, ] export function FlashSaleOfferBar({ saleTitle = 'Lightning Deal: Limited Quantity Batch', endTimeIso, claimedPercent = 78, itemsLeft = 14, tiers = defaultTiers, onSelectTier, onClaimDeal, className, }: FlashSaleProps) { const [selectedTierId, setSelectedTierId] = React.useState(tiers[1]?.id || tiers[0]?.id || '') const defaultEnd = React.useMemo(() => { return new Date(Date.now() + 6 * 3600 * 1000 + 42 * 60 * 1000 + 19 * 1000).toISOString() }, []) const targetTime = endTimeIso || defaultEnd const [timeLeft, setTimeLeft] = React.useState({ hours: '06', minutes: '42', seconds: '19', }) React.useEffect(() => { function updateCountdown() { const target = new Date(targetTime).getTime() const now = Date.now() const diff = Math.max(0, target - now) const h = Math.floor(diff / (1000 * 60 * 60)) const m = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)) const s = Math.floor((diff % (1000 * 60)) / 1000) setTimeLeft({ hours: String(h).padStart(2, '0'), minutes: String(m).padStart(2, '0'), seconds: String(s).padStart(2, '0'), }) } updateCountdown() const timer = setInterval(updateCountdown, 1000) return () => clearInterval(timer) }, [targetTime]) function handleSelect(tier: TierOffer) { setSelectedTierId(tier.id) onSelectTier?.(tier) } return ( <div data-slot="flash-sale-offer-bar" className={`border-border bg-card w-full overflow-hidden rounded-xl border shadow-xs ${className ?? ''}`} > {/* Deal Header Banner */} <div className="border-border bg-muted/40 flex flex-col gap-3 border-b p-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex items-center gap-2.5"> <div className="border-border bg-background flex size-8 items-center justify-center rounded-lg border shadow-2xs"> <Flame className="size-4 fill-orange-500/20 text-orange-500" /> </div> <div> <div className="flex items-center gap-2"> <span className="text-foreground text-sm font-semibold tracking-tight">{saleTitle}</span> <Badge variant="destructive" className="h-5 gap-1 px-1.5 font-mono text-xs uppercase"> <span className="bg-background size-1.5 animate-pulse rounded-full" /> Live </Badge> </div> <p className="text-muted-foreground text-xs">Price locks automatically once added to cart.</p> </div> </div> {/* Live Clock Digits */} <div className="flex items-center gap-2 self-start sm:self-auto"> <span className="text-muted-foreground text-xs font-medium">Ends in:</span> <div className="text-foreground flex items-center gap-1 font-mono text-xs font-semibold"> <span className="border-border bg-background rounded border px-1.5 py-0.5 shadow-2xs"> {timeLeft.hours}h </span> <span>:</span> <span className="border-border bg-background rounded border px-1.5 py-0.5 shadow-2xs"> {timeLeft.minutes}m </span> <span>:</span> <span className="border-border bg-background text-destructive rounded border px-1.5 py-0.5 shadow-2xs"> {timeLeft.seconds}s </span> </div> </div> </div> <div className="space-y-4 p-4"> {/* Claim Progress Bar */} <div className="space-y-1.5"> <div className="flex items-center justify-between text-xs"> <span className="text-foreground flex items-center gap-1.5 font-medium"> <Flame className="text-warning size-3.5" /> <span>{claimedPercent}% Claimed</span> </span> <span className="text-destructive font-mono font-medium">Only {itemsLeft} units remaining in stock</span> </div> <div className="bg-muted h-2 w-full overflow-hidden rounded-full"> <div className="h-full rounded-full bg-gradient-to-r from-orange-500 to-rose-500 transition-[width] duration-500" style={{ width: `${claimedPercent}%` }} /> </div> </div> {/* Tier Multi-Buy Cards Grid */} <div className="grid grid-cols-1 gap-2.5 sm:grid-cols-3"> {tiers.map((tier) => ( <button key={tier.id} type="button" className={`relative flex flex-col justify-between rounded-lg border p-3 text-left transition-colors ${ selectedTierId === tier.id ? 'border-primary bg-primary/5 ring-primary ring-1' : 'border-border bg-card hover:border-muted-foreground/40 hover:bg-muted/30' }`} onClick={() => handleSelect(tier)} > {/* Popular ribbon */} {tier.popular && ( <span className="border-border bg-primary text-primary-foreground absolute -top-2.5 right-3 rounded-full border px-2 py-0.5 text-xs font-semibold shadow-xs"> MOST POPULAR </span> )} <div className="space-y-1"> <div className="flex items-center justify-between"> <span className="text-foreground text-xs font-semibold"> Buy {tier.quantity} {tier.quantity > 1 ? 'Units' : 'Unit'} </span> <Badge variant="secondary" className="text-success font-mono text-xs font-bold"> -{tier.discountPct}% OFF </Badge> </div> <div className="text-muted-foreground text-xs font-medium">{tier.label}</div> </div> <div className="border-border/60 mt-3 flex items-center justify-between border-t pt-2 text-xs"> <span className="text-muted-foreground">{tier.badgeText}</span> <div className={`flex size-4 items-center justify-center rounded-full border text-xs font-bold ${ selectedTierId === tier.id ? 'border-primary bg-primary text-primary-foreground' : 'border-muted-foreground/40' }`} > ✓ </div> </div> </button> ))} </div> {/* Footer Guarantee & CTA Banner */} <div className="border-border bg-muted/20 flex flex-col gap-2 rounded-lg border p-3 text-xs sm:flex-row sm:items-center sm:justify-between"> <div className="text-muted-foreground flex items-center gap-2"> <ShieldCheck className="text-success size-4" /> <span>30-day price-match guarantee · Free express 2-day dispatch</span> </div> <Button size="sm" className="gap-1.5 text-xs shadow-xs" onClick={onClaimDeal}> <Tag className="size-3.5" /> <span>Apply Tier Discount to Cart</span> </Button> </div> </div> </div> ) }components/blocks/flash-sale-offer-bar/index.ts0.1 kB
export { FlashSaleOfferBar } from './FlashSaleOfferBar' export type { TierOffer, FlashSaleProps } from './FlashSaleOfferBar'
Raw manifest:https://uipkge.dev/r/react/flash-sale-offer-bar.json