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-bento
feature-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-bento

Source

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.

Feature bento, StoreKit