Tutorial · Intermediate · 9 min
A bento-box layout with CSS Grid
A bento layout packs tiles of different sizes into one tidy frame — a big feature panel next to a couple of small ones, a wide strip along the bottom, no gaps and no overlaps. The name comes from the partitioned Japanese lunch box. CSS Grid is built for exactly this: you define a grid once, then let individual tiles claim more columns or rows.
1. The base grid
Start with a fixed number of equal columns and let the rows size themselves. Four columns gives you enough
granularity to make some tiles twice as wide as others without the layout feeling rigid. grid-auto-rows
sets a baseline height for every row, and gap handles the spacing between tiles so you never
manage margins by hand.
.bento {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 160px;
gap: 16px;
}
.bento > * {
border-radius: 16px;
background: #f4f4f5;
padding: 20px;
} With nothing else, every child fills one column and one row — a plain 4-up grid. The bento character comes from letting a few tiles break that mold.
2. Let tiles span columns and rows
A tile claims more space with grid-column and grid-row. The span
keyword is the readable way to do it: span 2 means "take two tracks starting wherever this tile
lands." Make one feature tile two columns wide and two rows tall, then scatter a couple of mid-size tiles
around it. Grid's auto-placement flows the remaining single tiles into the gaps left over.
.tile--feature {
grid-column: span 2;
grid-row: span 2;
}
.tile--wide {
grid-column: span 2; /* two columns, one row */
}
.tile--tall {
grid-row: span 2; /* one column, two rows */
} The matching markup is just a list of tiles — the classes do the sizing:
<div class="bento">
<div class="tile tile--feature">Feature</div>
<div class="tile tile--tall">Tall</div>
<div class="tile">Small</div>
<div class="tile tile--wide">Wide</div>
<div class="tile">Small</div>
<div class="tile">Small</div>
</div> If a tile would overflow the four columns, Grid wraps it to the next row instead of letting it spill, so the
box stays intact even when the spans don't divide evenly. To pull denser tiles up into earlier gaps, add
grid-auto-flow: dense to the container — be aware it can reorder tiles visually away from their
source order, which matters for the accessibility note below.
3. Named areas, when you want exact control
Spanning is quick but leaves placement partly to auto-flow. When you want every tile in a known spot,
grid-template-areas draws the layout as ASCII art. You name each region, paint it across the
grid, then assign each tile to its name. This makes the structure readable at a glance and easy to rearrange.
.bento--areas {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 160px;
gap: 16px;
grid-template-areas:
"hero hero side top"
"hero hero side mid"
"foot foot foot mid";
}
.area-hero { grid-area: hero; }
.area-side { grid-area: side; }
.area-top { grid-area: top; }
.area-mid { grid-area: mid; }
.area-foot { grid-area: foot; } Read the strings like a map: hero occupies a 2×2 block top-left, foot is a wide
strip along the bottom, and the right edge stacks top, mid and side.
Each row string must have the same number of columns, and a name repeated across cells has to form a solid
rectangle — Grid rejects L-shapes. Changing the layout is now a matter of editing the picture.
4. Make it responsive
A four-column bento is too tight on a phone. Drop the column count at breakpoints. Tiles that span more columns than exist get clamped automatically, but it's cleaner to reset their spans so the layout reflows deliberately rather than by accident.
@media (max-width: 900px) {
.bento { grid-template-columns: repeat(2, 1fr); }
.tile--feature { grid-column: span 2; grid-row: span 2; }
.tile--wide { grid-column: span 2; }
}
@media (max-width: 560px) {
.bento { grid-template-columns: 1fr; }
.tile--feature,
.tile--wide,
.tile--tall { grid-column: auto; grid-row: auto; }
} For a layout that flexes without hard breakpoints, swap the fixed count for auto-fit and
minmax(). The browser fits as many columns as will hold a minimum width, then stretches them to
fill the row — tiles reflow on their own as the container resizes.
.bento--fluid {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
} One trade-off: with auto-fit you don't know how many columns exist at a given width, so a
span 2 tile can look different across sizes. Fluid grids suit galleries; for a controlled hero
layout, breakpoints give you the predictability you want.
5. Polish and accessibility
A single gap already keeps every seam equal, so resist adding margins on the tiles — they'd
double up against the gap. Round the corners on the tiles, not the container, and the bento reads as a set of
discrete cards. A light border or soft shadow per tile separates them on a busy page.
.tile {
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
} The accessibility point is about order. Screen readers and keyboard tab order follow the DOM, not the visual
grid — so write your tiles in the order they should be read, then position them visually with Grid.
Avoid using order or heavy reordering (including grid-auto-flow: dense) to fix the
look at the cost of a jumbled reading sequence. If the visual order genuinely is the meaningful order, change
the source to match rather than patching it in CSS. Keeping logical and visual order aligned is the difference
between a layout that's pretty and one that's usable.
Where to take it next
The same grid scales to dashboards, photo walls and pricing pages — anywhere uneven tiles need to sit in one
frame. Try mixing a fluid auto-fit section above a fixed hero block, or animate a tile's span on
hover for an expanding-card effect (transition grid-column on a grid that uses explicit line
numbers, since span values don't tween).
Want a starting layout without hand-placing every tile? Generate one in the Bento Grid Builder and copy the CSS. For more like this, browse more tutorials.