Hero
Hero Bento
Inner page opening above a grid of a large cover photo and two photo + fact rows.
Layout and motion from the Nitip template — see it in contextimport Link from "next/link";
import { cn } from "@/lib/utils";
/*
* Hero Bento — an inner page opening (breadcrumbs, heading, divider, description) above a grid of photos and fact
* cards: a large cover beside two rows of photo + fact.
* Source: marcframe/nitip → components/sections/AboutHeroSection.tsx (+ design-system CoverImage, FactCard).
*
* Layout kept from Framer: below 768px everything stacks (square cover, each row photo first). From 768px each fact
* row is 340px tall, photo and card side by side, and the second row puts the card first. From 1024px the grid is
* 700px tall: the cover fills the left half, the two rows share the right half. Cards: the first on the muted
* surface, the second on primary. No motion (Nitip's ring cursor is left out).
* Text over the cover photo is white on a black gradient in every theme — photos don't follow the theme.
*/
export type HeroBentoImage = { src: string; alt: string };
export type HeroBentoFact = { image: HeroBentoImage; title: string; value: string; caption?: string };
export type HeroBentoProps = {
/** Heading level: h1 when the hero opens the page. */
as?: "h1" | "h2";
/** Trail above the heading; the last item is the current page. Pass [] to leave it out. */
breadcrumbs?: { label: string; href?: string }[];
title?: string;
description?: string;
/** Large photo with a line of text over its bottom edge; `label` is a small line above it (a brand, a place). */
cover?: { image: HeroBentoImage; label?: string; text: string };
/** Two photo + fact rows. */
facts?: [HeroBentoFact, HeroBentoFact];
className?: string;
};
const photo = (id: string, w = 1400) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=${w}&q=80`;
const defaults = {
breadcrumbs: [{ label: "Home", href: "/" }, { label: "About" }],
title: "About us",
description:
"We don't just market — we build connections that grow. A team of strategists, creatives and analysts working as one with the brands we believe in.",
cover: {
image: { src: photo("photo-1542744173-8e7e53415bb0", 1600), alt: "Team meeting around a long table covered with laptops" },
label: "Northstar",
text: "Helping brands grow with digital work that performs.",
},
facts: [
{
image: { src: photo("photo-1460925895917-afdab827c52f", 1000), alt: "Laptop on a desk showing an analytics dashboard" },
title: "More than 100 campaigns delivered",
value: "30%",
caption: "Average rise in engagement within three months",
},
{
image: { src: photo("photo-1600880292203-757bb62b4baf", 1000), alt: "Two colleagues high-fiving across a desk" },
title: "Clients we helped raise",
value: "$10M+",
caption: "in funding across twelve startups",
},
] as [HeroBentoFact, HeroBentoFact],
};
export function HeroBento({
as: Heading = "h1",
breadcrumbs = defaults.breadcrumbs,
title = defaults.title,
description = defaults.description,
cover = defaults.cover,
facts = defaults.facts,
className,
}: HeroBentoProps) {
return (
<section className={cn("py-(--section-y)", className)}>
<div className="mx-auto flex max-w-(--container) flex-col gap-12 px-(--gutter)">
<div className="flex flex-col gap-8">
<div className="flex flex-col gap-3">
{breadcrumbs.length > 0 && (
<nav aria-label="Breadcrumb">
<ol className="flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground">
{breadcrumbs.map((crumb, i) => {
const last = i === breadcrumbs.length - 1;
return (
<li key={`${crumb.label}-${i}`} className="inline-flex items-center gap-1.5">
{crumb.href && !last ? (
<Link href={crumb.href} className="transition-colors hover:text-foreground">
{crumb.label}
</Link>
) : (
<span aria-current={last ? "page" : undefined} className={cn(last && "text-foreground")}>
{crumb.label}
</span>
)}
{!last && <span aria-hidden>/</span>}
</li>
);
})}
</ol>
</nav>
)}
<Heading className="font-heading text-h1 font-semibold tracking-tight text-balance">{title}</Heading>
</div>
<hr className="border-border" />
{description && <p className="max-w-xl text-lead text-pretty text-muted-foreground">{description}</p>}
</div>
<div className="flex flex-col gap-5 lg:h-175 lg:flex-row">
<div className="relative aspect-square overflow-hidden rounded-xl bg-muted md:aspect-[5/3] lg:aspect-auto lg:flex-1">
<img src={cover.image.src} alt={cover.image.alt} className="absolute inset-0 size-full object-cover" />
<div className="absolute inset-0 flex flex-col items-start justify-end gap-3 bg-gradient-to-b from-transparent via-black/5 to-black/60 p-5 text-white md:p-6">
{cover.label && <p className="font-heading text-lg font-semibold tracking-tight">{cover.label}</p>}
<p className="max-w-sm font-heading text-h3 font-semibold tracking-tight text-balance">{cover.text}</p>
</div>
</div>
<div className="flex flex-col gap-5 lg:flex-1">
{facts.map((fact, i) => (
<div key={fact.value} className="flex flex-col gap-5 md:h-85 md:flex-row lg:h-auto lg:flex-1">
<div
className={cn(
"relative aspect-[7/5] overflow-hidden rounded-xl bg-muted md:aspect-auto md:flex-1",
i === 1 && "md:order-last",
)}
>
<img src={fact.image.src} alt={fact.image.alt} className="absolute inset-0 size-full object-cover" />
</div>
<div
className={cn(
"flex h-75 flex-col items-start justify-between gap-6 rounded-xl p-5 md:h-auto md:flex-1 md:p-6",
i === 1 ? "bg-primary text-primary-foreground" : "bg-muted",
)}
>
<p className="font-heading text-h3 font-semibold tracking-tight text-balance">{fact.title}</p>
<div>
<p className="font-heading text-h1 font-semibold tracking-tight">{fact.value}</p>
{fact.caption && <p className="mt-1 max-w-64 text-sm opacity-70">{fact.caption}</p>}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</section>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/hero-bento.json1. Add the block tokens to
app/globals.cssonce (theming).npx shadcn@latest add https://framebrick.co/r/blocks/tokens.json2. Copy the block into
components/hero-bento.tsx.import Link from "next/link"; import { cn } from "@/lib/utils"; /* * Hero Bento — an inner page opening (breadcrumbs, heading, divider, description) above a grid of photos and fact * cards: a large cover beside two rows of photo + fact. * Source: marcframe/nitip → components/sections/AboutHeroSection.tsx (+ design-system CoverImage, FactCard). * * Layout kept from Framer: below 768px everything stacks (square cover, each row photo first). From 768px each fact * row is 340px tall, photo and card side by side, and the second row puts the card first. From 1024px the grid is * 700px tall: the cover fills the left half, the two rows share the right half. Cards: the first on the muted * surface, the second on primary. No motion (Nitip's ring cursor is left out). * Text over the cover photo is white on a black gradient in every theme — photos don't follow the theme. */ export type HeroBentoImage = { src: string; alt: string }; export type HeroBentoFact = { image: HeroBentoImage; title: string; value: string; caption?: string }; export type HeroBentoProps = { /** Heading level: h1 when the hero opens the page. */ as?: "h1" | "h2"; /** Trail above the heading; the last item is the current page. Pass [] to leave it out. */ breadcrumbs?: { label: string; href?: string }[]; title?: string; description?: string; /** Large photo with a line of text over its bottom edge; `label` is a small line above it (a brand, a place). */ cover?: { image: HeroBentoImage; label?: string; text: string }; /** Two photo + fact rows. */ facts?: [HeroBentoFact, HeroBentoFact]; className?: string; }; const photo = (id: string, w = 1400) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=${w}&q=80`; const defaults = { breadcrumbs: [{ label: "Home", href: "/" }, { label: "About" }], title: "About us", description: "We don't just market — we build connections that grow. A team of strategists, creatives and analysts working as one with the brands we believe in.", cover: { image: { src: photo("photo-1542744173-8e7e53415bb0", 1600), alt: "Team meeting around a long table covered with laptops" }, label: "Northstar", text: "Helping brands grow with digital work that performs.", }, facts: [ { image: { src: photo("photo-1460925895917-afdab827c52f", 1000), alt: "Laptop on a desk showing an analytics dashboard" }, title: "More than 100 campaigns delivered", value: "30%", caption: "Average rise in engagement within three months", }, { image: { src: photo("photo-1600880292203-757bb62b4baf", 1000), alt: "Two colleagues high-fiving across a desk" }, title: "Clients we helped raise", value: "$10M+", caption: "in funding across twelve startups", }, ] as [HeroBentoFact, HeroBentoFact], }; export function HeroBento({ as: Heading = "h1", breadcrumbs = defaults.breadcrumbs, title = defaults.title, description = defaults.description, cover = defaults.cover, facts = defaults.facts, className, }: HeroBentoProps) { return ( <section className={cn("py-(--section-y)", className)}> <div className="mx-auto flex max-w-(--container) flex-col gap-12 px-(--gutter)"> <div className="flex flex-col gap-8"> <div className="flex flex-col gap-3"> {breadcrumbs.length > 0 && ( <nav aria-label="Breadcrumb"> <ol className="flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground"> {breadcrumbs.map((crumb, i) => { const last = i === breadcrumbs.length - 1; return ( <li key={`${crumb.label}-${i}`} className="inline-flex items-center gap-1.5"> {crumb.href && !last ? ( <Link href={crumb.href} className="transition-colors hover:text-foreground"> {crumb.label} </Link> ) : ( <span aria-current={last ? "page" : undefined} className={cn(last && "text-foreground")}> {crumb.label} </span> )} {!last && <span aria-hidden>/</span>} </li> ); })} </ol> </nav> )} <Heading className="font-heading text-h1 font-semibold tracking-tight text-balance">{title}</Heading> </div> <hr className="border-border" /> {description && <p className="max-w-xl text-lead text-pretty text-muted-foreground">{description}</p>} </div> <div className="flex flex-col gap-5 lg:h-175 lg:flex-row"> <div className="relative aspect-square overflow-hidden rounded-xl bg-muted md:aspect-[5/3] lg:aspect-auto lg:flex-1"> <img src={cover.image.src} alt={cover.image.alt} className="absolute inset-0 size-full object-cover" /> <div className="absolute inset-0 flex flex-col items-start justify-end gap-3 bg-gradient-to-b from-transparent via-black/5 to-black/60 p-5 text-white md:p-6"> {cover.label && <p className="font-heading text-lg font-semibold tracking-tight">{cover.label}</p>} <p className="max-w-sm font-heading text-h3 font-semibold tracking-tight text-balance">{cover.text}</p> </div> </div> <div className="flex flex-col gap-5 lg:flex-1"> {facts.map((fact, i) => ( <div key={fact.value} className="flex flex-col gap-5 md:h-85 md:flex-row lg:h-auto lg:flex-1"> <div className={cn( "relative aspect-[7/5] overflow-hidden rounded-xl bg-muted md:aspect-auto md:flex-1", i === 1 && "md:order-last", )} > <img src={fact.image.src} alt={fact.image.alt} className="absolute inset-0 size-full object-cover" /> </div> <div className={cn( "flex h-75 flex-col items-start justify-between gap-6 rounded-xl p-5 md:h-auto md:flex-1 md:p-6", i === 1 ? "bg-primary text-primary-foreground" : "bg-muted", )} > <p className="font-heading text-h3 font-semibold tracking-tight text-balance">{fact.title}</p> <div> <p className="font-heading text-h1 font-semibold tracking-tight">{fact.value}</p> {fact.caption && <p className="mt-1 max-w-64 text-sm opacity-70">{fact.caption}</p>} </div> </div> </div> ))} </div> </div> </div> </section> ); }
Usage
import { HeroBento } from "@/components/hero-bento";
<HeroBento
title="About us"
cover={{ image: { src: "/about/team.jpg", alt: "Our team" }, label: "Northstar", text: "Helping brands grow." }}
facts={[
{ image: { src: "/about/1.jpg", alt: "…" }, title: "Campaigns delivered", value: "100+" },
{ image: { src: "/about/2.jpg", alt: "…" }, title: "Raised by our clients", value: "$10M+" },
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| as | "h1" | "h2" | "h1" | Heading level of the title. |
| breadcrumbs | { label, href? }[] | Home / About | Trail above the heading; [] hides it. |
| title / description | string | — | Page heading and the text under the divider. |
| cover | { image: { src, alt }, label?, text } | — | Large photo with a small label and a line of text over its bottom edge. |
| facts | [fact, fact] — { image, title, value, caption? } | — | Two rows: photo beside a card with a statement, a big figure and a caption. |
| className | string | — | Classes for the <section>. |
- Below 768px everything stacks; from 768px each fact row is 340px tall and the second row puts its card first; from 1024px the grid is 700px tall with the cover on the left.
- The first card uses the muted surface, the second primary. Text over the cover is white on a black gradient in every theme — photos don't follow the theme.
- No motion.
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS block called HeroBento (Next.js). - <section className="py-(--section-y)">, container mx-auto max-w-(--container) px-(--gutter), column gap-12. - Page title: breadcrumbs (text-sm muted, "/" separators), h1 font-heading text-h1 font-semibold tracking-tight, an <hr>, and a text-lead muted description max-w-xl. - Grid: column gap-5; from 1024px a 700px-tall row. Left: a cover photo (rounded-xl, square below 768px, 5:3 from 768px, fills the height from 1024px) with a bottom gradient (transparent → black/60) holding white text: a small label and a font-heading text-h3 line. Right: two rows (340px tall from 768px, equal share from 1024px), each a photo (7:5 on phones) beside a card (h-75 on phones) — the second row reversed from 768px. Card: rounded-xl p-6, statement (font-heading text-h3) at the top, a font-heading text-h1 figure and a small caption at the bottom; first card bg-muted, second bg-primary text-primary-foreground. - Only shadcn variables and block tokens; plain <img> with alt text. No motion.











