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

Source

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.

Trust strip hero, StoreKit