Dismissible promo bar
Promo strip with a copyable code chip and a remembered dismiss.
componentpromoanypromodiscount
$
npx shadcn@latest add @storekit/announcement-promoannouncement-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-promoSource
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.