Trust strip hero
Split product hero with a welded strip of three to five reassurances (shipping, returns, testing, rating) above or below it.
blocktrust-stripanyctatrust
$
npx shadcn@latest add @storekit/hero-trust-striphero-trust-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/hero-trust-strip$
pnpm dlx shadcn@latest add @storekit/hero-trust-strip$
yarn dlx shadcn@latest add @storekit/hero-trust-strip$
bunx --bun shadcn@latest add @storekit/hero-trust-stripSource
components/storekit/hero/hero-trust-strip.tsx
154 lines
"use client"
import { motion, useReducedMotion } from "motion/react"
import { FlaskConical, RotateCcw, ShieldCheck, Star, Truck, type LucideIcon } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"
/* Hero with an attached trust strip. A split product hero, then a strip of
three to five reassurances welded to it (shipping, returns, testing,
rating) so objections are answered before the first scroll. `stripAt`
puts the strip above or below the hero. On mobile the strip is a two
column grid, never a ticker, so every item stays readable. Icons come from
a small named set so the data stays serializable. */
const EASE = [0.23, 1, 0.32, 1] as const
type Cta = { label: string; href: string }
const ICONS: Record<string, LucideIcon> = { truck: Truck, returns: RotateCcw, lab: FlaskConical, shield: ShieldCheck, star: Star }
export type TrustItem = { icon: "truck" | "returns" | "lab" | "shield" | "star"; title: string; detail?: string; href?: string }
const SAMPLE: TrustItem[] = [
{ icon: "truck", title: "Free shipping over $50", detail: "Ships in 24 hours" },
{ icon: "returns", title: "30-day guarantee", detail: "Empty tub? Still refundable" },
{ icon: "lab", title: "Third-party tested", detail: "Every batch, results online" },
{ icon: "star", title: "4.8 from 12,400 reviews", detail: "Verified buyers only" },
]
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 HeroTrustStrip({
eyebrow = "Daily wellness",
headline = "Clean daily essentials, without the guesswork.",
subcopy = "Greens, electrolytes and sleep support, made in the USA and tested by a lab we don't own.",
primaryCta = { label: "Shop best sellers", href: "#" },
secondaryCta = { label: "Take the quiz", href: "#" },
image = "/placeholder.webp",
imageAlt = "",
items = SAMPLE,
stripAt = "below",
className,
}: {
eyebrow?: string
headline?: string
subcopy?: string
primaryCta?: Cta
secondaryCta?: Cta
image?: string
imageAlt?: string
items?: TrustItem[]
stripAt?: "above" | "below"
className?: string
}) {
const reduce = useReducedMotion()
const sk = useSkBlock("hero", "hero-trust-strip")
const list = items.slice(0, 5)
const n = list.length
const lastRowStart = n - (n % 2 || 2)
const strip = (
<ul
aria-label="Why shop with us"
className={cn(
"grid grid-cols-2 border-sk-line bg-sk-surface lg:grid-flow-col lg:auto-cols-fr lg:grid-cols-none",
stripAt === "below" ? "border-t" : "border-b"
)}
>
{list.map((it, i) => {
const Icon = ICONS[it.icon] ?? ShieldCheck
const body = (
<>
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-full bg-sk-paper text-sk-accent">
<Icon size={17} aria-hidden />
</span>
<span className="min-w-0">
<span className="block text-[13px] font-semibold leading-tight sm:text-sm">{it.title}</span>
{it.detail ? <span className="mt-0.5 block text-[12px] leading-tight text-sk-muted">{it.detail}</span> : null}
</span>
</>
)
return (
<motion.li
key={it.title}
initial={{ opacity: 0, y: reduce ? 0 : 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45, ease: EASE, delay: reduce ? 0 : 0.25 + i * 0.05 }}
className={cn(
"border-sk-line",
i % 2 === 0 && i < n - 1 && "border-r",
i < n - 1 && "lg:border-r",
i === n - 1 && "lg:border-r-0",
i < lastRowStart && "border-b lg:border-b-0"
)}
>
{it.href ? (
<a
href={it.href}
onClick={() => sk.emit("interact", "trust", i)}
className="flex h-full flex-col items-start gap-2 px-4 py-4 sm:flex-row sm:items-center sm:gap-3 outline-none transition-colors hover:bg-sk-ink/[0.03] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-sk-accent sm:px-5"
>
{body}
</a>
) : (
<div className="flex h-full flex-col items-start gap-2 px-4 py-4 sm:flex-row sm:items-center sm:gap-3 sm:px-5">{body}</div>
)}
</motion.li>
)
})}
</ul>
)
return (
<section ref={sk.ref} {...sk.attrs} className={cn("bg-sk-paper text-sk-ink", className)}>
<div className="mx-auto w-full max-w-6xl px-4 py-6 sm:px-8 sm:py-8">
<div className="overflow-hidden rounded-sk-card border border-sk-line">
{stripAt === "above" ? strip : null}
<div className="grid items-center lg:grid-cols-2">
<div className="order-2 p-6 sm:p-10 lg:order-1 lg:p-14">
<motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.4 }} className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">
{eyebrow}
</motion.p>
<motion.h1
initial={{ opacity: 0, y: reduce ? 0 : 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: EASE, delay: reduce ? 0 : 0.05 }}
className="mt-4 text-balance font-sk-display font-semibold leading-[1.08] tracking-[-0.02em]"
style={{ fontSize: "clamp(2rem, 3.8vw, 3rem)" }}
>
{headline}
</motion.h1>
<p className="mt-4 max-w-md text-[15px] leading-[1.55] text-sk-muted">{subcopy}</p>
<div className="mt-8 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>
</div>
<div className="relative order-1 aspect-[4/3] bg-sk-surface lg:order-2 lg:aspect-auto lg:h-full lg:min-h-[480px]">
<img src={image} alt={imageAlt} className="absolute inset-0 h-full w-full object-cover" />
</div>
</div>
{stripAt === "below" ? strip : null}
</div>
</div>
</section>
)
}
Usage
Import from "@/components/storekit/hero/hero-trust-strip" and pass your product data. The demo above exercises the full prop surface with sample values.
