A bento grid of photo tiles, one large and four small, with numbers, captions and a hover arrow; swipeable on phones. React and Tailwind CSS.
BentoPhotoTiles.tsx
// Bento photo tiles: one large tile and four small ones in a 4×2 grid, each
// numbered, with a caption and a lime arrow that appears on hover; swipes on phones.
// Pure Tailwind, no dependencies. Works with Tailwind v3 and v4.
type Tile = { title: string; caption?: string; image: string; href?: string };
type Props = {
heading?: string;
intro?: string;
tiles?: Tile[];
};
const img = (id: string) => `https://images.unsplash.com/photo-${id}?w=1200&q=80&auto=format&fit=crop`;
const DEFAULT_TILES: Tile[] = [
{ title: "The farm in autumn", caption: "Harvest walks every Saturday", image: img("1774533566123-faecfb3020eb") },
{ title: "Pasture", caption: "Grass-fed, all year", image: img("1784807650061-70f963106154") },
{ title: "The woods", caption: "Forty acres left wild", image: img("1441974231531-c6227db76b6e") },
{ title: "The farmhouse", caption: "Lunch from the fields", image: img("1558036117-15d82a90b9b1") },
{ title: "Sunrise rounds", caption: "Where the day starts", image: img("1582300857444-5ddd87c86797") },
];
export default function BentoTiles({
heading = "Life on the farm",
intro = "Five places you'll want to see when you visit.",
tiles = DEFAULT_TILES,
}: Props) {
return (
<section className="bg-[#f6f0de] px-4 py-24 font-['Geist',ui-sans-serif,system-ui,sans-serif] text-[#0f1712] antialiased md:px-8 md:py-36">
<div className="mx-auto max-w-7xl">
<div className="grid gap-6 md:grid-cols-12 md:items-end">
<h2 className="font-['Newsreader',Georgia,serif] text-[clamp(2.25rem,4.5vw,4.5rem)] leading-none md:col-span-7">{heading}</h2>
<p className="text-lg text-[#0f1712]/65 md:col-span-4 md:col-start-9">{intro}</p>
</div>
<ul className="-mx-4 mt-14 flex snap-x snap-mandatory gap-3 overflow-x-auto px-4 pb-2 [scrollbar-width:none] md:mx-0 md:mt-20 md:grid md:h-[44rem] md:grid-cols-4 md:grid-rows-2 md:gap-4 md:overflow-visible md:px-0 md:pb-0 [&::-webkit-scrollbar]:hidden">
{tiles.map((tile, i) => {
const big = i === 0;
return (
<li key={tile.title} className={`h-[28rem] w-[78vw] shrink-0 snap-start sm:w-[50vw] md:h-auto md:w-auto ${big ? "md:col-span-2 md:row-span-2" : ""}`}>
<a href={tile.href ?? "#"} className="group relative block h-full overflow-hidden rounded-2xl bg-[#012f1d]">
<img src={tile.image} alt={tile.title} loading="lazy" className="absolute inset-0 h-full w-full object-cover transition-transform duration-[1600ms] ease-[cubic-bezier(0.22,1,0.36,1)] group-hover:scale-105" />
<div className="absolute inset-0 bg-gradient-to-t from-[#011f13]/80 via-[#011f13]/10 to-transparent" />
<span className="absolute left-4 top-4 rounded-full bg-[#011f13]/35 px-3 py-2 text-xs tabular-nums text-[#f6f0de] backdrop-blur-md">{String(i + 1).padStart(2, "0")}</span>
<span className="absolute right-4 top-4 grid h-11 w-11 scale-75 place-items-center rounded-full bg-lime-400 text-[#0f1712] opacity-0 transition-all duration-500 group-hover:scale-100 group-hover:opacity-100">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.75} aria-hidden className="h-4 w-4">
<path d="M7 17 17 7M8 7h9v9" />
</svg>
</span>
<div className="absolute inset-x-5 bottom-5 text-[#f6f0de] md:inset-x-6 md:bottom-6">
<h3 className={`font-['Newsreader',Georgia,serif] leading-tight ${big ? "text-[clamp(1.875rem,3vw,3rem)]" : "text-3xl"}`}>{tile.title}</h3>
{tile.caption && <p className="mt-2 text-sm text-[#f6f0de]/75">{tile.caption}</p>}
</div>
</a>
</li>
);
})}
</ul>
</div>
</section>
);
}
How to use it
- Make sure your Next.js or React project has Tailwind CSS set up (v3 or v4 both work).
- Create
components/BentoPhotoTiles.tsxand paste the code. - Import it into a page and replace the sample text and images with your own.
