Macro detail hero
Hard crop into one texture or ingredient with a zoom and focal point, a corner copy card and a mono caption.
blockmacrobeautysupplementsanycta
$
npx shadcn@latest add @storekit/hero-macrohero-macro
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-macro$
pnpm dlx shadcn@latest add @storekit/hero-macro$
yarn dlx shadcn@latest add @storekit/hero-macro$
bunx --bun shadcn@latest add @storekit/hero-macroSource
components/storekit/hero/hero-macro.tsx
86 lines
"use client"
import { motion, useReducedMotion } from "motion/react"
import { cn } from "@/components/storekit/lib/cn"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"
/* Macro product hero. The image is cropped hard into one detail (label,
powder, cap texture) with `zoom` and a `focus` point, and a small copy card
sits in the lower corner on the paper token so it reads on any photo. A
mono caption names what the crop shows. Under md the card drops below the
crop instead of covering it. */
const EASE = [0.23, 1, 0.32, 1] as const
type Cta = { label: string; href: string }
export function HeroMacro({
image = "/placeholder.webp",
imageAlt = "",
zoom = 2.2,
focus = { x: 50, y: 62 },
caption = "Detail: Daily Greens, one scoop",
eyebrow = "Up close",
headline = "Real greens look like this.",
subcopy = "Deep color, fine grind, no fillers. It mixes smooth in water with a quick shake.",
primaryCta = { label: "Shop Daily Greens", href: "#" },
className,
}: {
image?: string
imageAlt?: string
/** Scale factor for the crop. 1 shows the whole image. */
zoom?: number
/** Focal point in percent of the image. */
focus?: { x: number; y: number }
caption?: string
eyebrow?: string
headline?: string
subcopy?: string
primaryCta?: Cta
className?: string
}) {
const reduce = useReducedMotion()
const sk = useSkBlock("hero", "hero-macro")
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-8 sm:px-8 sm:py-12">
<div className="relative">
<div className="relative aspect-[4/5] overflow-hidden rounded-sk-card border border-sk-line bg-sk-surface sm:aspect-[16/10] lg:aspect-[16/8]">
<img
src={image}
alt={imageAlt}
className="absolute inset-0 h-full w-full object-cover"
style={{ transform: `scale(${zoom})`, transformOrigin: `${focus.x}% ${focus.y}%` }}
/>
<p className="absolute right-3 top-3 rounded-sk-chip bg-[rgb(0_0_0/0.55)] px-2.5 py-1 font-sk-mono text-[11px] uppercase tracking-[0.14em] text-[rgb(255_255_255)]">
{zoom.toFixed(1)}x
</p>
</div>
<motion.div
initial={{ opacity: 0, y: reduce ? 0 : 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: EASE, delay: reduce ? 0 : 0.15 }}
className="relative mt-4 rounded-sk-card border border-sk-line bg-sk-paper p-5 sm:p-6 md:absolute md:bottom-6 md:left-6 md:mt-0 md:max-w-sm"
>
<p className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">{eyebrow}</p>
<h1 className="mt-3 text-balance font-sk-display text-[28px] font-semibold leading-[1.1] tracking-[-0.02em]">
{headline}
</h1>
<p className="mt-3 text-sm leading-[1.5] text-sk-muted">{subcopy}</p>
<a
href={primaryCta.href}
onClick={() => sk.emit("interact", "cta")}
className="sk-btn mt-5 inline-flex h-11 w-full items-center justify-center rounded-sk-btn bg-sk-accent px-6 text-sm 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] sm:w-auto"
>
{primaryCta.label}
</a>
</motion.div>
</div>
<p className="mt-3 font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted md:text-right">{caption}</p>
</div>
</section>
)
}
Usage
Set `zoom` and `focus` to crop into the detail; the card drops below the crop under md.
