Dismissible promo bar

Promo strip with a copyable code chip and a remembered dismiss.

componentpromoanypromodiscount
$npx shadcn@latest add @storekit/announcement-promo
announcement-promo

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/announcement-promo
$pnpm dlx shadcn@latest add @storekit/announcement-promo
$yarn dlx shadcn@latest add @storekit/announcement-promo
$bunx --bun shadcn@latest add @storekit/announcement-promo

Source

components/storekit/announcement-bar/announcement-promo.tsx
110 lines
"use client"

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

/* Dismissible promo bar with a copyable code chip. Dismissal collapses the
   bar's height and is remembered in localStorage under `storageKey`. Storage
   access is guarded, so private windows simply show the bar again. */

export function AnnouncementPromo({
  message = "First order: 15% off your daily essentials",
  code = "MORNING15",
  storageKey = "sk-promo-dismissed",
  className,
}: {
  message?: string
  code?: string
  storageKey?: string
  className?: string
}) {
  const reduce = useReducedMotion()
  const sk = useSkBlock("announcement-bar", "announcement-promo")
  const [open, setOpen] = useState(false)
  const [copied, setCopied] = useState(false)

  useEffect(() => {
    let dismissed = false
    try {
      dismissed = window.localStorage.getItem(storageKey) === "1"
    } catch {}
    setOpen(!dismissed)
  }, [storageKey])

  useEffect(() => {
    if (!copied) return
    const t = window.setTimeout(() => setCopied(false), 1200)
    return () => window.clearTimeout(t)
  }, [copied])

  const copy = async () => {
    try {
      await navigator.clipboard.writeText(code)
    } catch {}
    setCopied(true)
    sk.emit("interact", "copy_code")
  }

  const dismiss = () => {
    setOpen(false)
    try {
      window.localStorage.setItem(storageKey, "1")
    } catch {}
  }

  return (
    <div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs}>
      <AnimatePresence initial={false}>
        {open ? (
          <motion.div
            key="promo"
            role="region"
            aria-label="Promotion"
            initial={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
            animate={reduce ? { opacity: 1 } : { height: "auto", opacity: 1 }}
            exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
            transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
            className={cn("overflow-hidden bg-sk-bar text-sk-bar-ink", className)}
          >
            <div className="relative flex min-h-11 items-center justify-center gap-3 py-1.5 pl-4 pr-12 text-[13px]">
              <p className="min-w-0 leading-[1.35] text-balance">{message}</p>
              <button
                type="button"
                onClick={copy}
                aria-label={copied ? "Code copied" : `Copy code ${code}`}
                className="relative inline-flex h-7 shrink-0 items-center gap-1.5 rounded-sk-chip before:absolute before:-inset-y-1.5 before:inset-x-0 before:content-[''] border border-dashed border-sk-bar-ink/40 px-2.5 font-sk-mono text-[11px] font-semibold uppercase tracking-[0.14em] outline-none transition-colors duration-200 hover:bg-sk-bar-ink/10 active:scale-[0.97] focus-visible:ring-2 focus-visible:ring-sk-bar-ink focus-visible:ring-offset-2 focus-visible:ring-offset-sk-bar"
              >
                <AnimatePresence initial={false} mode="wait">
                  <motion.span
                    key={copied ? "copied" : "code"}
                    initial={{ opacity: 0, y: reduce ? 0 : 4 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={{ opacity: 0, y: reduce ? 0 : -4 }}
                    transition={{ duration: 0.18 }}
                    className="inline-flex items-center gap-1.5"
                  >
                    {copied ? <Check size={12} aria-hidden /> : <Copy size={12} aria-hidden />}
                    {copied ? "Copied" : code}
                  </motion.span>
                </AnimatePresence>
              </button>
              <span aria-live="polite" className="sr-only">{copied ? "Code copied" : ""}</span>
              <button
                type="button"
                onClick={dismiss}
                aria-label="Dismiss promotion"
                className="absolute right-0 top-1/2 grid h-11 w-11 -translate-y-1/2 place-items-center opacity-70 outline-none transition-opacity duration-200 hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-sk-bar-ink"
              >
                <X size={16} aria-hidden />
              </button>
            </div>
          </motion.div>
        ) : null}
      </AnimatePresence>
    </div>
  )
}

Usage

Import from "@/components/storekit/announcement-bar/announcement-promo" and pass your product data. The demo above exercises the full prop surface with sample values.

Dismissible promo bar, StoreKit