Founder or creator endorsement hero

Portrait card with a product chip beside a large pull quote, name, role, credentials and an FTC disclosure line for paid partners.

blockendorsementanycta
$npx shadcn@latest add @storekit/hero-endorsement
hero-endorsement

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-endorsement
$pnpm dlx shadcn@latest add @storekit/hero-endorsement
$yarn dlx shadcn@latest add @storekit/hero-endorsement
$bunx --bun shadcn@latest add @storekit/hero-endorsement

Source

components/storekit/hero/hero-endorsement.tsx
132 lines
"use client"

import { motion, useReducedMotion } from "motion/react"
import { Quote } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"

/* Creator or founder endorsement hero. A portrait card on one side, a large
   pull quote on the other, then the person's name, role and the credentials
   that make the quote count. `kind` switches the small label between a
   founder note and a creator or expert partner, and a disclosure line is
   rendered for paid partnerships (FTC). The product they are holding links
   from a chip pinned to the portrait. Stacks portrait first on mobile. */

const EASE = [0.23, 1, 0.32, 1] as const

type Cta = { label: string; href: string }

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 HeroEndorsement({
  kind = "founder",
  headline = "Why we make it",
  portrait = "/placeholder.webp",
  portraitAlt = "",
  portraitFocus = "center",
  quote = "I built this because I wanted one thing in my morning I never had to think about. Every batch still crosses my desk before it ships.",
  name = "Jordan Ellis",
  role = "Founder",
  credentials = ["Registered dietitian", "12 years in clinical nutrition"],
  product,
  disclosure,
  primaryCta = { label: "Shop the routine", href: "#" },
  secondaryCta = { label: "Read the full story", href: "#" },
  className,
}: {
  kind?: "founder" | "creator" | "expert"
  /** Page heading for assistive tech; the quote is the visual headline. */
  headline?: string
  portrait?: string
  portraitAlt?: string
  portraitFocus?: string
  quote?: string
  name?: string
  role?: string
  credentials?: string[]
  /** Product in the shot, shown as a chip on the portrait. */
  product?: { title: string; image?: string; href: string }
  /** Paid partnership or gifted product note. Required by the FTC for creator deals. */
  disclosure?: string
  primaryCta?: Cta
  secondaryCta?: Cta
  className?: string
}) {
  const reduce = useReducedMotion()
  const sk = useSkBlock("hero", "hero-endorsement")
  const label = kind === "founder" ? "A note from our founder" : kind === "expert" ? "Expert pick" : "Creator pick"
  const rise = (i: number) => ({
    initial: { opacity: 0, y: reduce ? 0 : 8 },
    animate: { opacity: 1, y: 0 },
    transition: { duration: 0.5, ease: EASE, delay: reduce ? 0 : 0.12 + i * 0.07 },
  })

  return (
    <section ref={sk.ref} {...sk.attrs} className={cn("bg-sk-paper text-sk-ink", className)}>
      <div className="mx-auto grid w-full max-w-6xl items-center gap-8 px-4 py-10 sm:px-8 sm:py-14 lg:grid-cols-[0.85fr_1.15fr] lg:gap-16 lg:py-16">
        <motion.figure
          initial={{ opacity: 0, scale: reduce ? 1 : 0.97 }}
          animate={{ opacity: 1, scale: 1 }}
          transition={{ duration: 0.6, ease: EASE }}
          className="relative mx-auto w-full max-w-[440px] lg:max-w-none"
        >
          <div className="relative aspect-[4/5] overflow-hidden rounded-sk-card border border-sk-line bg-sk-surface">
            <img src={portrait} alt={portraitAlt} className="absolute inset-0 h-full w-full object-cover" style={{ objectPosition: portraitFocus }} />
          </div>
          {product ? (
            <a
              href={product.href}
              onClick={() => sk.emit("interact", "product_chip")}
              className="absolute -bottom-4 right-4 flex items-center gap-3 rounded-sk-card border border-sk-line bg-sk-card py-2 pl-2 pr-4 text-sk-ink shadow-[0_24px_48px_-24px_rgb(var(--sk-ink)/0.35)] outline-none transition-transform duration-300 ease-sk-out hover:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-sk-accent"
            >
              {product.image ? <img src={product.image} alt="" className="h-11 w-11 rounded-sk-chip bg-sk-surface object-cover" /> : null}
              <span className="flex flex-col leading-tight">
                <span className="font-sk-mono text-[10px] uppercase tracking-[0.14em] text-sk-muted">In the shot</span>
                <span className="text-sm font-medium">{product.title}</span>
              </span>
            </a>
          ) : null}
        </motion.figure>

        <div className="pt-4 lg:pt-0">
          <h1 className="sr-only">{headline}</h1>
          <motion.p {...rise(0)} className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">
            {label}
          </motion.p>
          <motion.blockquote {...rise(1)} className="relative mt-5">
            <Quote aria-hidden size={28} className="mb-3 text-sk-accent" />
            <p className="text-balance font-sk-display font-semibold leading-[1.15] tracking-[-0.015em]" style={{ fontSize: "clamp(1.6rem, 3.2vw, 2.6rem)" }}>
              {quote}
            </p>
          </motion.blockquote>
          <motion.div {...rise(2)} className="mt-7 flex flex-col gap-1">
            <p className="text-base font-semibold">{name}</p>
            <p className="text-sm text-sk-muted">{role}</p>
            {credentials.length ? (
              <ul className="mt-3 flex flex-wrap gap-2">
                {credentials.map((c) => (
                  <li key={c} className="rounded-sk-chip border border-sk-line px-3 py-1 text-[12px] text-sk-muted">
                    {c}
                  </li>
                ))}
              </ul>
            ) : null}
          </motion.div>
          <motion.div {...rise(3)} 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>
          </motion.div>
          {disclosure ? <p className="mt-5 text-[12px] text-sk-muted">{disclosure}</p> : null}
        </div>
      </div>
    </section>
  )
}

Usage

Set `kind` to founder, creator or expert. Always pass `disclosure` for paid or gifted creator deals.

Founder or creator endorsement hero, StoreKit