Feature bento
Five tiles of different kinds: product image, stat, customer quote and two benefits. Staggered in, 2px hover lift.
blockbentoanybenefitstrustsocial-proof
$
npx shadcn@latest add @storekit/feature-bentofeature-bento
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/feature-bento$
pnpm dlx shadcn@latest add @storekit/feature-bento$
yarn dlx shadcn@latest add @storekit/feature-bento$
bunx --bun shadcn@latest add @storekit/feature-bentoSource
components/storekit/marketing/feature-bento.tsx
123 lines
"use client"
import { motion, useReducedMotion } from "motion/react"
import { RotateCcw, Truck, type LucideIcon } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { ProductImage } from "@/components/storekit/ui/product-image"
import { Stars } from "@/components/storekit/ui/stars"
/* Benefit bento, 3 x 2 on desktop. Five tiles of different kinds: a wide
product image tile, a stat, a customer quote and two short benefits.
Tiles rise in staggered on view and lift 2px on hover. */
type Benefit = { icon: LucideIcon; title: string; body: string }
const EASE: [number, number, number, number] = [0.16, 1, 0.3, 1]
const BENEFITS: Benefit[] = [
{ icon: Truck, title: "Subscribe and save", body: "15% off every delivery. Skip or cancel any time." },
{ icon: RotateCcw, title: "30-day guarantee", body: "Not for you? Get your money back, even if the pouch is empty." },
]
export function FeatureBento({
eyebrow = "Why people reorder",
title = "Made to drink daily, not to sit in a cupboard.",
image = "/brands/morrow/greens.webp",
imageTitle = "One scoop, 28 greens, no grassy aftertaste.",
imageBody = "Scan the code on the pouch to see the third-party lab results for your batch.",
stat = "94%",
statEyebrow = "Subscriber survey",
statLabel = "of subscribers say they have more steady energy after two weeks",
quote = "It actually tastes good, mixes in ten seconds, and my afternoon slump is gone.",
quoteAuthor = "Priya N.",
quoteRole = "Verified buyer",
benefits = BENEFITS,
className,
}: {
eyebrow?: string
title?: string
image?: string
imageTitle?: string
imageBody?: string
stat?: string
statEyebrow?: string
statLabel?: string
quote?: string
quoteAuthor?: string
quoteRole?: string
benefits?: Benefit[]
className?: string
}) {
const reduce = useReducedMotion()
const tile = "rounded-sk-card border border-sk-line bg-sk-card transition-transform duration-200 ease-sk-out hover:-translate-y-0.5 motion-reduce:hover:translate-y-0"
const item = { hidden: { opacity: 0, y: reduce ? 0 : 8 }, show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: EASE } } }
return (
<section data-sk-section="marketing" data-sk-variant="feature-bento" className={cn("w-full px-4 py-16 sm:px-8 sm:py-24", className)}>
<div className="mx-auto max-w-6xl">
<p className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">{eyebrow}</p>
<h2 className="mt-3 max-w-xl font-sk-display text-3xl font-semibold leading-[1.1] tracking-[-0.02em] sm:text-4xl">{title}</h2>
<motion.ul
className="mt-10 grid gap-3 sm:grid-cols-2 lg:grid-cols-3"
initial="hidden"
whileInView="show"
viewport={{ once: true, amount: 0.2 }}
variants={{ hidden: {}, show: { transition: { staggerChildren: reduce ? 0 : 0.06 } } }}
>
{/* Image tile */}
<motion.li variants={item} className="sm:col-span-2">
<div className={cn(tile, "grid h-full overflow-hidden sm:grid-cols-[1fr_1fr]")}>
<div className="flex flex-col justify-end gap-2 p-6 sm:order-1">
<p className="font-sk-display text-xl font-semibold leading-[1.1] tracking-[-0.02em]">{imageTitle}</p>
<p className="text-sm leading-[1.5] text-sk-muted">{imageBody}</p>
</div>
<ProductImage src={image} alt="" fit="cover" className="min-h-[220px] sm:order-2 sm:aspect-auto sm:h-full" />
</div>
</motion.li>
{/* Stat tile */}
<motion.li variants={item}>
<div className={cn(tile, "flex h-full flex-col justify-between gap-8 bg-sk-surface p-6")}>
<p className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">{statEyebrow}</p>
<div>
<p className="font-sk-display text-5xl font-semibold leading-[1.1] tracking-[-0.02em] tabular-nums text-sk-accent">{stat}</p>
<p className="mt-2 text-sm leading-[1.5] text-sk-muted">{statLabel}</p>
</div>
</div>
</motion.li>
{/* Quote tile */}
<motion.li variants={item}>
<figure className={cn(tile, "flex h-full flex-col justify-between gap-6 p-6")}>
<div>
<Stars value={5} size={12} />
<blockquote className="mt-3 text-[15px] leading-[1.5] text-sk-ink">{quote}</blockquote>
</div>
<figcaption className="text-sm">
<span className="font-medium text-sk-ink">{quoteAuthor}</span>
<span className="text-sk-muted"> / {quoteRole}</span>
</figcaption>
</figure>
</motion.li>
{/* Benefit tiles */}
{benefits.slice(0, 2).map(({ icon: Icon, title: t, body }) => (
<motion.li key={t} variants={item}>
<div className={cn(tile, "flex h-full flex-col gap-6 p-6")}>
<span className="grid h-10 w-10 place-items-center rounded-sk-chip bg-sk-accent/10 text-sk-accent">
<Icon size={18} />
</span>
<div>
<p className="text-[15px] font-semibold text-sk-ink">{t}</p>
<p className="mt-1 text-sm leading-[1.5] text-sk-muted">{body}</p>
</div>
</div>
</motion.li>
))}
</motion.ul>
</div>
</section>
)
}
Usage
Import from "@/components/storekit/marketing/feature-bento" and pass your product data. The demo above exercises the full prop surface with sample values.
