Offer-led hero

The discount is the headline: oversized offer figure, tap-to-copy promo code, eligibility line and a tagged packshot.

blockofferanyctapromo
$npx shadcn@latest add @storekit/hero-offer
hero-offer

Live and interactive. Switch the brand in the frame header to see the same source under another token file.

Installation

$npx shadcn@latest add @storekit/hero-offer
$pnpm dlx shadcn@latest add @storekit/hero-offer
$yarn dlx shadcn@latest add @storekit/hero-offer
$bunx --bun shadcn@latest add @storekit/hero-offer

Source

components/storekit/hero/hero-offer.tsx
150 lines
"use client"

import { useRef, useState } from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { Check, Copy } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"

/* Offer-led hero. The offer is the headline: a large figure ("25% off"),
   what it applies to, a promo code that copies on tap, and the eligibility
   line right under it so nobody feels tricked at checkout. `autoApplied`
   swaps the code for an "applied at checkout" note. The packshot sits on
   the surface token with a sale tag. Stacks offer first on mobile. */

const EASE = [0.23, 1, 0.32, 1] as const

type Cta = { label: string; href: string }

const PRIMARY =
  "sk-btn inline-flex min-h-12 items-center justify-center py-2.5 text-center leading-tight rounded-sk-btn bg-sk-accent px-7 text-[15px] font-medium text-sk-accent-ink outline-none transition-[filter,transform] duration-200 ease-sk-out hover:brightness-[1.06] focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper active:scale-[0.97]"
const SECONDARY =
  "sk-btn inline-flex min-h-12 items-center justify-center py-2.5 text-center leading-tight rounded-sk-btn border border-sk-line px-7 text-[15px] font-medium text-sk-ink outline-none transition-[background-color,transform] duration-200 ease-sk-out hover:bg-sk-ink/[0.04] focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper active:scale-[0.97]"

export function HeroOffer({
  eyebrow = "Autumn reset sale",
  figure = "25% off",
  headline = "Everything on the shelf.",
  subcopy = "Stack it with Subscribe and Save for up to 35% off your first order.",
  code = "RESET25",
  autoApplied = false,
  eligibility = "Ends Sunday at 11:59pm ET. Excludes gift cards. One use per customer.",
  primaryCta = { label: "Shop the sale", href: "#" },
  secondaryCta = { label: "Shop bundles", href: "#" },
  image = "/placeholder.webp",
  imageAlt = "",
  tag = "Sale",
  className,
}: {
  eyebrow?: string
  /** The offer itself, set very large. */
  figure?: string
  headline?: string
  subcopy?: string
  code?: string
  /** True when the discount applies without a code. */
  autoApplied?: boolean
  eligibility?: string
  primaryCta?: Cta
  secondaryCta?: Cta
  image?: string
  imageAlt?: string
  tag?: string
  className?: string
}) {
  const reduce = useReducedMotion()
  const sk = useSkBlock("hero", "hero-offer")
  const [copied, setCopied] = useState(false)
  const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(code)
    } catch {
      /* clipboard can be blocked; the code is still visible to type */
    }
    setCopied(true)
    sk.emit("interact", "copy_code")
    clearTimeout(timer.current)
    timer.current = setTimeout(() => setCopied(false), 2000)
  }

  return (
    <section ref={sk.ref} {...sk.attrs} className={cn("bg-sk-paper text-sk-ink", className)}>
      <div className="mx-auto grid w-full max-w-6xl items-center gap-10 px-4 py-10 sm:px-8 sm:py-14 lg:grid-cols-[1.1fr_0.9fr] lg:gap-14">
        <motion.div initial={{ opacity: 0, y: reduce ? 0 : 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, ease: EASE }}>
          <p className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-sale">{eyebrow}</p>
          <h1 className="mt-3">
            <span className="block font-sk-display font-semibold leading-[0.92] tracking-[-0.04em] text-sk-ink" style={{ fontSize: "clamp(3.75rem, 11vw, 8.5rem)" }}>
              {figure}
            </span>
            <span className="mt-3 block text-balance font-sk-display font-semibold leading-[1.1] tracking-[-0.02em]" style={{ fontSize: "clamp(1.5rem, 2.8vw, 2.25rem)" }}>
              {headline}
            </span>
          </h1>
          <p className="mt-4 max-w-md text-[15px] leading-[1.55] text-sk-muted">{subcopy}</p>

          <div className="mt-7 flex flex-wrap items-center gap-3">
            {autoApplied ? (
              <p className="sk-btn inline-flex h-12 items-center gap-2 rounded-sk-btn border border-dashed border-sk-savings/50 bg-sk-savings/[0.08] px-4 text-sm font-medium text-sk-savings">
                <Check size={16} aria-hidden /> Discount applied at checkout
              </p>
            ) : (
              <button
                type="button"
                onClick={copy}
                className="group inline-flex h-12 items-center gap-3 rounded-sk-btn border border-dashed border-sk-ink/30 bg-sk-surface pl-4 pr-3 outline-none transition-[border-color,transform] duration-200 ease-sk-out hover:border-sk-ink/60 focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper active:scale-[0.98]"
              >
                <span className="text-[12px] text-sk-muted">Code</span>
                <span className="font-sk-mono text-[15px] font-medium tracking-[0.08em] text-sk-ink">{code}</span>
                <span className="relative grid h-8 w-8 place-items-center rounded-full bg-sk-ink/[0.06] text-sk-ink">
                  <AnimatePresence initial={false} mode="popLayout">
                    <motion.span
                      key={copied ? "y" : "n"}
                      initial={{ opacity: 0, scale: reduce ? 1 : 0.6 }}
                      animate={{ opacity: 1, scale: 1 }}
                      exit={{ opacity: 0, scale: reduce ? 1 : 0.6 }}
                      transition={{ type: "spring", stiffness: 500, damping: 30 }}
                      className="grid place-items-center"
                    >
                      {copied ? <Check size={15} aria-hidden /> : <Copy size={14} aria-hidden />}
                    </motion.span>
                  </AnimatePresence>
                </span>
                <span className="sr-only">{copied ? "Copied" : "Copy code"}</span>
              </button>
            )}
            <span aria-live="polite" className={cn("text-[13px] text-sk-savings transition-opacity duration-200", copied ? "opacity-100" : "opacity-0")}>
              {copied ? "Copied to clipboard" : ""}
            </span>
          </div>

          <div className="mt-6 flex flex-col gap-3 sm:flex-row">
            <a href={primaryCta.href} onClick={() => sk.emit("interact", "cta")} className={PRIMARY}>
              {primaryCta.label}
            </a>
            <a href={secondaryCta.href} onClick={() => sk.emit("interact", "cta_secondary")} className={SECONDARY}>
              {secondaryCta.label}
            </a>
          </div>
          {eligibility ? <p className="mt-5 max-w-md text-[12px] leading-[1.5] text-sk-muted">{eligibility}</p> : null}
        </motion.div>

        <motion.div
          initial={{ opacity: 0, scale: reduce ? 1 : 0.96 }}
          animate={{ opacity: 1, scale: 1 }}
          transition={{ duration: 0.6, ease: EASE, delay: reduce ? 0 : 0.1 }}
          className="relative mx-auto aspect-square w-full max-w-[460px] overflow-hidden rounded-sk-card border border-sk-line bg-sk-surface"
        >
          <img src={image} alt={imageAlt} className="absolute inset-0 h-full w-full object-cover" />
          {tag ? (
            <span className="absolute left-4 top-4 rotate-[-6deg] rounded-sk-chip bg-sk-sale px-3 py-1.5 font-sk-mono text-[12px] font-medium uppercase tracking-[0.12em] text-sk-paper shadow-[0_10px_24px_-12px_rgb(var(--sk-ink)/0.5)]">
              {tag}
            </span>
          ) : null}
        </motion.div>
      </div>
    </section>
  )
}

Usage

Keep `eligibility` honest and visible. Set `autoApplied` when no code is needed.

Offer-led hero, StoreKit