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-strip
upsell-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-strip

Source

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.

Pre-CTA policy strip, StoreKit