Promo Page React template for Tailwind CSS v4
v1.0.0Added in 1.9.0
A limited-time sale page: a drawn coupon hero with a countdown and copyable code, plan cards with live totals and a savings calculator.
The hero rises in once on mount beside a drawn composition: a perforated coupon for 40% off with notches, a barcode stub and the code, in front of two gift tags with the Pro and Business deal prices and a starburst sticker. The countdown ticks from a fixed clock (so server and client agree) to the end of the sale, or to its start in the teaser phase. Claim the deal jumps to the plans; the dashed code chip copies ACME40 with a check and a screen reader announcement. A progress bar shows how many onboarding spots are claimed. Each plan card shows the deal price beside the crossed-out regular one; its seats stepper (buttons, arrows, typing) rolls the first-year total and the savings live. Claim spins for 900 ms (or while onClaim runs), then shows the code applied with the seats and total and a Continue to checkout link, or an error if onClaim rejects; Change seats goes back. In the savings calculator the team size slider and the Pro / Business switch roll the amount saved and the two price bars; Use these seats copies them into that plan's card and scrolls to it. Bonus tiles, quotes from last year's sale, plain-English terms (one open at a time) and the FAQ follow, then a closing band with its own countdown. Teaser swaps the offer for a validated notify-me form (an error under the field, then success) with the number blurred on the coupon; last call turns the pill, label and countdown red; ended stamps the coupon, shows regular prices and a waitlist form. On phones a bar pinned to the bottom (above the safe area) shows the lowest price and the time left once the hero scrolls away, with a Claim (or Notify me) button. Reduced motion keeps everything still.
Built with
The sections this page is made of. In the full-screen preview, hold Alt to outline each one on the page.
Add to a project
npx wingo-ui@latest add promo-pagePro item: the CLI needs a Pro account. See plans
Usage
Import PromoPage from @/components/templates/promo-page.
// app/(marketing)/deals/page.tsx
"use client"
import Link from "next/link"
import { getPromoPhase, PromoPage } from "@/components/templates/promo-page"
import { PROMO_ENDS_AT, PROMO_STARTS_AT } from "@/components/templates/promo-page-data"
export default function DealsRoute() {
return (
<PromoPage
linkComponent={Link}
phase={getPromoPhase(PROMO_STARTS_AT, PROMO_ENDS_AT)}
now={null}
ctaTone="brand"
onClaim={async (plan, seats) => {
const response = await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ plan: plan.id, seats, code: "ACME40" }) })
if (!response.ok) throw new Error("Could not apply the deal")
}}
onNotify={async (email) => {
await fetch("/api/sale-alerts", { method: "POST", body: JSON.stringify({ email }) })
}}
/>
)
}Props
designdefault"classic""classic" | "bold" | "minimal"The whole page's look. classic: a split hero with the coupon and gift tags over a sunburst, a dark closing band; bold: the all-black Black Friday page in both themes, a pill header, a giant title in the offer color under a night-blue glow, offer-colored countdown tiles, rounder cards, a quote wall, FAQ cards, an offer-colored closing band and a wordmark footer; minimal: a transparent header, a centered hero with the coupon alone, outlined flat cards, no bands, a ruled closing band and a simple footer. The *Props overrides still win.
phasedefault"live""teaser" | "live" | "last-call" | "ended"Where the sale is. teaser: a countdown to the start, a notify-me form and locked plan cards; live: the offer with a countdown to the end; last-call: the same with red urgency in the pill, label, countdown and phone bar; ended: a stamped coupon, regular prices, a waitlist form and no calculator, bonuses or terms. getPromoPhase computes it from the dates.
discountdefault40numberPercent off. It flows into every label ({discount}), the coupon, the stickers and every price and total.
codedefault"ACME40"stringThe promo code on the coupon, the copy chips, the checkout link and the labels ({code}).
startsAtdefault"2026-11-27T08:00:00Z"Date | number | stringWhen the sale starts (Nov 27 at 12:00 AM PT): the teaser countdown and {start} in the copy.
endsAtdefault"2026-12-02T07:59:59Z"Date | number | stringWhen the sale ends (Dec 1 at 11:59 PM PT): the countdowns and {end} in the copy.
nowdefault"PROMO_SAMPLE_NOW[phase]"Date | number | string | nullThe clock the countdowns start from. The default is a fixed sample per phase (a week before, mid-sale, the final hours, the day after) so server and client render the same digits; null uses the visitor's real clock.
tickdefaulttruebooleanThe countdowns run on from now after hydration; false freezes them (screenshots).
timeZonedefault"America/Los_Angeles"stringThe IANA zone the dates in the copy are formatted in, so every visitor reads the same day.
plansdefault"PROMO_PLANS"PromoPlan[]The offer cards: { id: "pro" | "business", name, description, yearlyMonthly (regular price per user per month, billed yearly), monthly, featuresTitle?, features, seats (on load), badge?, featured? }.
extrasdefault"PROMO_EXTRAS"PromoExtra[]The bonus tiles: { id, icon?, tag, title, description, value }.
testimonialsdefault"PROMO_TESTIMONIALS"Testimonial[]Quotes from last year's sale: { quote, name, role?, company?, companyIcon?, metric? }.
termsdefault"PROMO_TERMS"PromoTerm[]The plain-English terms: { id, title, content }; strings may hold {discount}, {code}, {start} and {end}.
faqdefault"PROMO_FAQ"FaqItem[]{ question, answer } pairs; string answers may hold the same placeholders.
spotsdefault"{ claimed: 2418, total: 5000 }"PromoSpotsThe onboarding spots progress in the hero.
labelsdefault"DEFAULT_PROMO_PAGE_LABELS"Partial<PromoPageLabels>Every visible string, with {discount}, {code}, {start}, {end} and {year} filled in (a few take more, like {plan} or {seats}).
hrefsdefault"DEFAULT_PROMO_PAGE_HREFS"Partial<PromoPageHrefs>{ checkout, pricing, sales, terms }. The checkout link gets ?plan=&seats=&code= appended.
defaultSeatsdefault25numberThe savings calculator's team size on load.
defaultPlandefault"business""pro" | "business"The savings calculator's plan on load.
maxSeatsdefault200numberThe most seats the steppers and the calculator's slider go to.
showAnnouncementdefaulttruebooleanThe strip above the header bar.
showCountdowndefaulttruebooleanThe countdowns in the hero and the closing band.
showCodedefaulttruebooleanThe copyable code chips.
showSpotsdefaulttruebooleanThe onboarding spots progress in the hero (live and last call).
showPlansdefaulttruebooleanThe plan cards.
showCalculatordefaulttruebooleanThe savings calculator (hidden once the sale ended).
showExtrasdefaulttruebooleanThe bonus tiles (hidden once the sale ended).
showTestimonialsdefaulttruebooleanThe quotes from last year's sale.
showTermsdefaulttruebooleanThe plain-English terms (hidden once the sale ended).
showFaqdefaulttruebooleanThe FAQ.
showCtadefaulttruebooleanThe closing band.
showStickyBardefaulttruebooleanThe phone bar pinned to the bottom once the hero scrolls away (below 768px, not when ended).
alternatedefaulttruebooleanBands in the bandBackground look on every other visible section. Applies to the classic and bold designs.
backgrounddefault"aurora""plain" | "soft" | "glow" | "aurora" | "grid" | "dots"The main background: plain is the bare page color; the others tint the page and add a glow, an aurora, a grid or dots over the first screen. A design picks its own unless you set one. classic uses aurora, bold a glow with gradient bands, minimal plain; on a plain page the bold hero draws its own offer-colored spotlight.
backgroundColordefault"oklch(0.62 0.14 275)"stringThe tint and glow color of the main background.
bandBackgrounddefault"muted""muted" | "tinted" | "gradient" | "dots"The secondary background of the alternating bands. Cards, fields and tiles inside a band are lifted one step above it, so they never share its color.
bandColordefault"oklch(0.62 0.14 275)"stringThe tint of tinted and gradient bands; defaults to backgroundColor.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Checks, the slider, the spots bar, badges and the offer color when offerColor is empty.
ctaTonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Claim buttons, the notify form, the phone bar and the header's Get started.
offerColordefault"#FFD43B"stringThe festive color of the coupon, the savings stickers, the deal bar and the bold design's title and closing band. Text on it turns black or white by lightness. Empty follows tone.
headerdefaulttruebooleanThe marketing header; off inside a site layout.
footerdefaulttruebooleanThe marketing footer with its newsletter form.
headerPropsPartial<MarketingHeaderProps>Passed to MarketingHeader.
footerPropsPartial<MarketingFooterProps>Passed to MarketingFooter.
testimonialsPropsPartial<TestimonialsSectionProps>Overrides for the TestimonialsSection.
faqPropsPartial<FaqSectionProps>Overrides for the FaqSection.
onClaim(plan: PromoPlan, seats: number) => (void | Promise<unknown>)A plan's Claim button. A promise keeps it spinning, a rejection shows an error. Without it: 900 ms, then the code applied and a checkout link.
onNotify(email: string) => (void | Promise<unknown>)The notify-me (teaser) and waitlist (ended) forms; a rejection shows an error. Without it: 900 ms, then success.
onCopyCode(code: string) => voidA code chip copied the code.
onSubscribe(email: string) => (void | Promise<unknown>)The footer newsletter; a rejected promise shows an error.
animateIndefaulttruebooleanThe hero rises in once and the coupon settles into place; false keeps the page still (thumbnails). Reduced motion always does.
localedefault"en-US"stringIntl locale for prices, totals, counters and dates. Empty follows UIProvider.
currencydefault"USD"stringISO currency for every price; the plan amounts are taken as is.
linkComponentdefault"<a>"(props: LinkComponentProps) => ReactNodeThe router link for every link on the page (next/link in an app).
classNamestringClasses for the root.
classNamesPartial<Record<PromoPageSlot, string>>Classes for header, hero, pill, title, art, code, status, countdown, spots, notify, plans, plan, calculator, extras, extra, testimonials, terms, faq, cta, sticky and footer.
Parts
getPromoPhase
functionThe phase a sale is in at a moment: (startsAt, endsAt, now = Date.now(), lastCallHours = 24) => "teaser" | "live" | "last-call" | "ended". Pair it with now={null} for a page that follows the real clock.
PROMO_PAGE_DESIGNS
constantThe design names: classic, bold and minimal.
DEFAULT_PROMO_PAGE_LABELS
constantThe English strings with their placeholders, to spread under a translation.
DEFAULT_PROMO_PAGE_HREFS
constant/checkout, /pricing, /contact and /legal/terms.
PROMO_PAGE_SAMPLE
constantAll the sample content in one object: discount, code, dates, time zone, sample clocks, spots, plans, extras, quotes, terms and FAQ.
PROMO_PLANS
constantPro ($10 per user per month billed yearly, 10 seats) and Business ($20, 25 seats, Best value).
PROMO_EXTRAS
constantAcme AI at half price for three months, a free onboarding session and a white-glove migration.
PROMO_TESTIMONIALS
constantThree quotes from last year's sale with a metric each.
PROMO_TERMS
constantWho can claim, how long the price lasts, seats, refunds, stacking and the deadline.
PROMO_FAQ
constantFive questions about the code, billing, current plans, seat limits and next year.
PROMO_SAMPLE_NOW
constantThe fixed sample clock per phase: Nov 20, Nov 29, the afternoon of Dec 1 and Dec 3, 2026.
PROMO_SPOTS
constant{ claimed: 2418, total: 5000 }.
PROMO_CODE
constant"ACME40".
PROMO_DISCOUNT
constant40.
PROMO_STARTS_AT
constant"2026-11-27T08:00:00Z", Nov 27 at 12:00 AM PT.
PROMO_ENDS_AT
constant"2026-12-02T07:59:59Z", Dec 1 at 11:59 PM PT.
PROMO_TIME_ZONE
constant"America/Los_Angeles".
PromoPageProps
typeProps of PromoPage.
PromoPageDesign
type"classic" | "bold" | "minimal".
PromoPagePhase
type"teaser" | "live" | "last-call" | "ended".
PromoPlan
typeOne offer card.
PromoPlanId
type"pro" | "business".
PromoExtra
typeOne bonus tile.
PromoTerm
typeOne plain-English term.
PromoSpots
type{ claimed, total }.
PromoPageLabels
typeThe strings labels can override.
PromoPageHrefs
typeThe link targets hrefs can override.
PromoPageSlot
typeThe classNames keys.
Examples
Bold design
design="bold": the all-black Black Friday page in both themes, a giant title in the offer color, offer-colored countdown tiles and closing band.
Minimal design
design="minimal": a transparent header, a centered hero with the coupon alone, outlined cards, no gray bands and a simple footer.
Teaser before the sale
phase="teaser": a countdown to Nov 27, a blurred number on the coupon, a validated notify-me form and locked plan cards.
Last call in brand orange
phase="last-call" with ctaTone brand: red urgency on the pill, label and countdown, final hours left.
Sale ended
phase="ended": a stamped coupon, regular prices with links to pricing and a waitlist for next year.
Cyber Monday at 30% off, checkout down
discount 30 with code CYBER30 flows into every string and price; an onClaim that rejects shows the error in the card.
Dependencies
Files
- components/templates/promo-page.tsx
- components/templates/promo-page-data.ts