Pre-CTA policy strip
Delivery window, returns and guarantee in one row above add to cart, each opening a short detail.
componentpolicy-stripanytrustshipping
$
npx shadcn@latest add @storekit/upsell-policy-stripupsell-policy-strip
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/upsell-policy-strip$
pnpm dlx shadcn@latest add @storekit/upsell-policy-strip$
yarn dlx shadcn@latest add @storekit/upsell-policy-strip$
bunx --bun shadcn@latest add @storekit/upsell-policy-stripSource
components/storekit/upsell/upsell-policy-strip.tsx
147 lines
"use client"
import { useEffect, useId, useState } from "react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import { ChevronDown, RotateCcw, ShieldCheck, Truck } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"
/* Pre-CTA policy strip. Delivery, returns and guarantee in one compact row
placed directly above the add to cart button, each specific rather than
generic. Tapping an item opens its one-paragraph detail underneath. The
delivery window is computed from business days after mount, so server and
client markup match. */
const EASE = [0.23, 1, 0.32, 1] as const
const RING = "outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper"
type Policy = { id: string; icon: React.ComponentType<{ size?: number; className?: string }>; label: string; value: string; detail: string }
function addBusinessDays(from: Date, n: number) {
const d = new Date(from)
let left = n
while (left > 0) {
d.setDate(d.getDate() + 1)
const day = d.getDay()
if (day !== 0 && day !== 6) left--
}
return d
}
export function UpsellPolicyStrip({
deliveryDays = [2, 4],
returnsDays = 30,
guarantee = "30-day guarantee",
details,
className,
}: {
/** Business-day delivery window [min, max] from today. */
deliveryDays?: [number, number]
returnsDays?: number
guarantee?: string
/** Override the detail paragraph per policy id: delivery, returns, guarantee. */
details?: Partial<Record<"delivery" | "returns" | "guarantee", string>>
className?: string
}) {
const reduce = useReducedMotion()
const uid = useId()
const sk = useSkBlock("upsell", "upsell-policy-strip")
const [window_, setWindow] = useState<string | null>(null)
const [open, setOpen] = useState<string | null>(null)
const [minDays, maxDays] = deliveryDays
useEffect(() => {
const fmt = (d: Date) => d.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" }).replace(/ (\d)/g, "\u00a0$1")
const now = new Date()
setWindow(`${fmt(addBusinessDays(now, minDays))} to ${fmt(addBusinessDays(now, maxDays))}`)
}, [minDays, maxDays])
const policies: Policy[] = [
{
id: "delivery",
icon: Truck,
label: "Delivery",
value: window_ ?? `${deliveryDays[0]} to ${deliveryDays[1]} business days`,
detail: details?.delivery ?? `Tracked shipping in ${deliveryDays[0]} to ${deliveryDays[1]} business days. Free on orders over $150, otherwise calculated at checkout.`,
},
{
id: "returns",
icon: RotateCcw,
label: "Returns",
value: `Free ${returnsDays}-day returns`,
detail: details?.returns ?? `Unopened items can be returned within ${returnsDays} days of delivery. We email a prepaid label and refund to your original payment method.`,
},
{
id: "guarantee",
icon: ShieldCheck,
label: "Guarantee",
value: guarantee,
detail: details?.guarantee ?? "Every batch is third-party tested. Not happy within 30 days? We refund you, opened or not.",
},
]
const active = policies.find((p) => p.id === open)
return (
<div {...sk.attrs} ref={sk.ref as React.RefObject<HTMLDivElement>} className={cn("w-full overflow-hidden rounded-sk-card border border-sk-line bg-sk-card", className)}>
<ul className="grid grid-cols-3 divide-x divide-sk-line">
{policies.map(({ id, icon: Icon, label, value }) => {
const on = open === id
return (
<li key={id} className="min-w-0">
<button
type="button"
aria-expanded={on}
aria-controls={`${uid}-detail`}
onClick={() => {
if (!on) sk.emit("interact", `policy_${id}`, undefined)
setOpen(on ? null : id)
}}
className={cn(
"flex h-full min-h-[72px] w-full flex-col items-start gap-1.5 px-3 py-3 text-left transition-[background-color,transform] duration-200 hover:bg-sk-ink/[0.03] active:scale-[0.97] sm:px-4",
on && "bg-sk-surface",
RING
)}
>
<span className="flex w-full items-center gap-1.5 text-sk-muted">
<Icon size={14} className="shrink-0" />
<span className="sr-only truncate font-sk-mono text-[11px] uppercase tracking-[0.14em] sm:not-sr-only">{label}</span>
<motion.span animate={{ rotate: on ? 180 : 0 }} transition={{ duration: reduce ? 0 : 0.25, ease: EASE }} className="ml-auto hidden shrink-0 sm:block">
<ChevronDown size={14} />
</motion.span>
</span>
<span className="max-w-full break-words text-[13px] font-medium leading-[1.35] text-sk-ink tabular-nums">{value}</span>
</button>
</li>
)
})}
</ul>
<AnimatePresence initial={false}>
{active ? (
<motion.div
id={`${uid}-detail`}
key="detail"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: reduce ? 0 : 0.3, ease: EASE }}
className="overflow-hidden"
>
<AnimatePresence mode="wait" initial={false}>
<motion.p
key={active.id}
initial={{ opacity: 0, y: reduce ? 0 : 6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: reduce ? 0 : 0.25, ease: EASE }}
className="border-t border-sk-line px-4 py-3 text-[13px] leading-[1.5] text-sk-muted"
>
{active.detail}
</motion.p>
</AnimatePresence>
</motion.div>
) : null}
</AnimatePresence>
</div>
)
}
Usage
Import from "@/components/storekit/upsell/upsell-policy-strip" and pass your product data. The demo above exercises the full prop surface with sample values.