Every product page I open this year is a bento grid — Apple, Vercel, Linear, Raycast, half the AI launches. The name comes from the Japanese lunch box: one tray, compartments of different sizes, each holding one thing. I always assumed there was some layout library behind it. There isn't. It's plain CSS Grid used with a bit of intent, and once I saw the recipe I stopped reaching for anything else. So I built a live one — switch the column count, drag the gap, randomize the spans, toggle a track overlay — with no framework and no background image. Every tile is real HTML. Here's the whole recipe.
Equal column tracks, then give the rows a height
Everything starts with one element set to display:grid and four equal columns:
.bento {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 4 equal, fluid columns */
grid-auto-rows: 96px; /* so a row-span MEANS something */
}
1fr is "one fraction of the free space", so the columns always add up to 100% with no percentages or math, and the direct children become grid items that auto-place into the next free cell. The line people forget is grid-auto-rows. You declared the columns but not the rows — grid creates rows implicitly as tiles flow in, and by default each is only as tall as its content, which makes a "row span" meaningless (spanning two short rows just gives you two short rows). Pin a height (or minmax(96px, auto) if you want them to grow) and suddenly spanning two rows is visibly twice as tall. That single line is what makes the vertical half of a bento work.
Spans are the whole trick — and the hero is 2×2
A tile occupies one cell by default. To make it bigger you just tell it how many tracks to cover:
.wide { grid-column: span 2; } /* 2 columns wide */
.tall { grid-row: span 2; } /* 2 rows tall */
.hero { grid-column: span 2; grid-row: span 2; } /* the 2×2 focal tile */
That's it — that's what turns a boring uniform grid into a bento with hierarchy. The hero is the single oversized tile that anchors the composition and gives the eye an entry point. I use span N rather than pinning exact lines (1 / 3) because span stays correct no matter where auto-placement drops the tile.
The gap gives it rhythm; dense packing fills the holes
One property does most of the visual work: gap. It sets space between tracks without adding any edge margin, so tiles never touch and the grid stays flush. This is why you never use margins for grid spacing. The other nicety is packing — mix tile sizes and auto-placement strands empty cells when a wide tile won't fit at the end of a row. grid-auto-flow: dense sends the grid back to back-fill those holes with later, smaller tiles:
.bento { grid-auto-flow: row dense; } /* back-fill gaps varied spans leave */
The one caveat I flag in the demo: dense can reorder tiles visually away from source order, which matters for keyboard and screen-reader users — so keep the DOM order sensible.
Reflow — two ways, and my default
A four-column bento is unusable on a phone, so it has to collapse. The explicit way is media queries dropping to 2 columns, then 1 — you don't touch the tiles at all, CSS just clamps any span wider than the grid. But my default now needs zero breakpoints:
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
Read it as "fit as many columns as you can, each at least 150px, share the leftover equally". As the container narrows it drops a column on its own; widen it and it adds one. It responds to its container, not the viewport — exactly what you want inside a resizable panel.
The thing I took away: a bento isn't a trend, it's legible. The hero is hierarchy, the shared gap and radius are rhythm, and tile area maps to importance so big things look big. Keep it honest — one hero, a limited palette, a sane order — and it reads in seconds.
Play with it — flip the columns, drag the gap, hit Randomize spans:
https://dev48v.infy.uk/design/day46-bento-grid.html
Top comments (0)