Open any design portfolio, SaaS landing page, or Apple keynote and you will see the same layout: a grid of tiles with varying sizes, each containing a self-contained piece of content. This is the bento grid — named after the Japanese bento box where food is arranged in neat compartments.
It is not a new concept, but in 2026 it has become the dominant UI pattern for feature pages, dashboards, and portfolios. Let me show you how to build them properly.
What Makes a Bento Grid
A bento grid is a CSS Grid layout where items span different numbers of rows and columns, creating an asymmetric but balanced composition. Unlike a masonry layout (where items flow vertically with varying heights), a bento grid uses explicit placement — every tile has a deliberate size and position.
┌─────────────────────┬───────────┐
│ │ │
│ Large Feature │ Small │
│ Tile │ Tile │
│ │ │
├───────────┬─────────┴───────────┤
│ │ │
│ Small │ Medium Tile │
│ Tile │ │
│ │ │
├───────────┼───────────┬─────────┤
│ │ │ │
│ Medium │ Small │ Small │
│ Tile │ Tile │ Tile │
│ │ │ │
└───────────┴───────────┴─────────┘The key characteristics:
- Fixed grid structure. Tiles align to a defined grid, not free-flowing.
- Varied tile sizes. At least 2-3 different tile sizes create visual hierarchy.
- Self-contained tiles. Each tile is a complete unit — icon, heading, description, or visual.
- Gaps between tiles. Consistent gutters separate each compartment.
- No tile is purely decorative. Every compartment communicates something.
Building a Bento Grid with CSS Grid
The Foundation
Start with a grid container. The number of columns depends on your design, but 4 columns is the sweet spot for most layouts.
.bento-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 180px;
gap: 16px;
max-width: 1200px;
margin: 0 auto;
padding: 16px;
}grid-auto-rows: 180px sets a consistent row height. Tiles that span 2 rows will be 376px tall (180 + 16 gap + 180). This ensures vertical rhythm.
Tile Sizing Classes
Define classes for the tile sizes you need:
/* 1x1 — Small tile */
.bento-tile {
background: #f5f5f5;
padding: 24px;
overflow: hidden;
position: relative;
}
/* 2x1 — Wide tile */
.bento-tile--wide {
grid-column: span 2;
}
/* 1x2 — Tall tile */
.bento-tile--tall {
grid-row: span 2;
}
/* 2x2 — Large feature tile */
.bento-tile--large {
grid-column: span 2;
grid-row: span 2;
}Complete Starter Layout
Here is a full, copy-paste-ready bento grid:
<div class="bento-grid">
<div class="bento-tile bento-tile--large">
<h2>Ship Faster</h2>
<p>Deploy with confidence using automated canary analysis.</p>
<img src="/feature-hero.svg" alt="" />
</div>
<div class="bento-tile">
<span class="bento-stat">99.9%</span>
<p>Uptime SLA</p>
</div>
<div class="bento-tile">
<span class="bento-stat">50ms</span>
<p>Median latency</p>
</div>
<div class="bento-tile bento-tile--wide">
<h3>Global Edge Network</h3>
<p>Content delivered from 200+ locations worldwide.</p>
</div>
<div class="bento-tile bento-tile--tall">
<h3>Integrations</h3>
<ul>
<li>GitHub</li>
<li>GitLab</li>
<li>Bitbucket</li>
<li>Slack</li>
<li>Discord</li>
</ul>
</div>
<div class="bento-tile">
<h3>Analytics</h3>
<p>Real-time insights into your application.</p>
</div>
<div class="bento-tile">
<h3>Security</h3>
<p>SOC 2 Type II certified.</p>
</div>
</div>.bento-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 180px;
gap: 16px;
max-width: 1200px;
margin: 0 auto;
padding: 16px;
}
.bento-tile {
background: #ffffff;
border: 1px solid #e5e5e5;
padding: 24px;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.bento-tile h2 {
font-size: 1.75rem;
font-weight: 700;
margin: 0 0 8px;
}
.bento-tile h3 {
font-size: 1.125rem;
font-weight: 600;
margin: 0 0 8px;
}
.bento-tile p {
color: #666;
font-size: 0.9rem;
margin: 0;
}
.bento-tile img {
position: absolute;
bottom: -10%;
right: -5%;
width: 60%;
opacity: 0.9;
}
.bento-stat {
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.bento-tile--wide {
grid-column: span 2;
}
.bento-tile--tall {
grid-row: span 2;
}
.bento-tile--large {
grid-column: span 2;
grid-row: span 2;
}
/* Dark variant */
.bento-tile--dark {
background: #1a1a1a;
border-color: #333;
color: #ffffff;
}
.bento-tile--dark p {
color: #999;
}
/* Accent variant */
.bento-tile--accent {
background: #E21B1B;
border-color: #E21B1B;
color: #ffffff;
}
.bento-tile--accent p {
color: rgba(255, 255, 255, 0.8);
}Making It Responsive
Here is the critical part most tutorials skip. A 4-column bento grid does not work on mobile. You need breakpoints that restructure the grid, not just shrink it.
/* Mobile: single column, auto height */
@media (max-width: 639px) {
.bento-grid {
grid-template-columns: 1fr;
grid-auto-rows: auto;
}
.bento-tile--wide,
.bento-tile--tall,
.bento-tile--large {
grid-column: span 1;
grid-row: span 1;
}
.bento-tile {
min-height: 160px;
}
.bento-tile--large {
min-height: 280px;
}
}
/* Tablet: 2 columns */
@media (min-width: 640px) and (max-width: 1023px) {
.bento-grid {
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 160px;
}
.bento-tile--wide {
grid-column: span 2;
}
.bento-tile--large {
grid-column: span 2;
grid-row: span 2;
}
/* Tall tiles become normal height on tablet */
.bento-tile--tall {
grid-row: span 1;
min-height: 200px;
}
}
/* Desktop: full 4 columns */
@media (min-width: 1024px) {
.bento-grid {
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 180px;
}
}Responsive Strategy: Priorities
The responsive approach depends on your content:
| Strategy | When to Use | How It Works |
|---|---|---|
| Collapse to single column | Content-heavy tiles | All tiles become full-width, stacked vertically |
| Reduce columns | Visual/stat tiles | 4 cols -> 2 cols -> 1 col at breakpoints |
| Hide non-essential tiles | Feature pages | Show only primary tiles on mobile with display: none |
| Reorder tiles | Hero/CTA focused | Use order property to prioritize important tiles |
For reordering on mobile:
@media (max-width: 639px) {
.bento-tile--hero { order: -2; }
.bento-tile--cta { order: -1; }
/* Everything else stays in document order */
}Building with Tailwind CSS
If you are using Tailwind (and in 2026, you probably are), here is the same layout without writing custom CSS:
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4
auto-rows-[180px] gap-4 max-w-5xl mx-auto p-4">
<!-- Large feature tile (2x2) -->
<div class="sm:col-span-2 sm:row-span-2 bg-white dark:bg-neutral-900
border border-neutral-200 dark:border-neutral-800
p-6 flex flex-col justify-between relative overflow-hidden">
<div>
<h2 class="text-2xl font-bold">Ship Faster</h2>
<p class="text-neutral-500 mt-2">
Deploy with confidence using automated canary analysis.
</p>
</div>
<img src="/feature-hero.svg" alt=""
class="absolute -bottom-[10%] -right-[5%] w-3/5 opacity-90" />
</div>
<!-- Stat tile -->
<div class="bg-white dark:bg-neutral-900 border border-neutral-200
dark:border-neutral-800 p-6 flex flex-col justify-between">
<span class="text-4xl font-extrabold tracking-tight">99.9%</span>
<p class="text-neutral-500 text-sm">Uptime SLA</p>
</div>
<!-- Stat tile -->
<div class="bg-white dark:bg-neutral-900 border border-neutral-200
dark:border-neutral-800 p-6 flex flex-col justify-between">
<span class="text-4xl font-extrabold tracking-tight">50ms</span>
<p class="text-neutral-500 text-sm">Median latency</p>
</div>
<!-- Wide tile (2x1) -->
<div class="sm:col-span-2 bg-white dark:bg-neutral-900
border border-neutral-200 dark:border-neutral-800 p-6">
<h3 class="text-lg font-semibold">Global Edge Network</h3>
<p class="text-neutral-500 text-sm mt-2">
Content delivered from 200+ locations worldwide.
</p>
</div>
<!-- Tall tile (1x2) -->
<div class="sm:row-span-2 bg-neutral-900 text-white
border border-neutral-800 p-6 flex flex-col">
<h3 class="text-lg font-semibold">Integrations</h3>
<ul class="mt-4 space-y-2 text-sm text-neutral-400">
<li>GitHub</li>
<li>GitLab</li>
<li>Bitbucket</li>
<li>Slack</li>
<li>Discord</li>
</ul>
</div>
<!-- Small tiles -->
<div class="bg-white dark:bg-neutral-900 border border-neutral-200
dark:border-neutral-800 p-6">
<h3 class="text-lg font-semibold">Analytics</h3>
<p class="text-neutral-500 text-sm mt-2">Real-time insights.</p>
</div>
<div class="bg-white dark:bg-neutral-900 border border-neutral-200
dark:border-neutral-800 p-6">
<h3 class="text-lg font-semibold">Security</h3>
<p class="text-neutral-500 text-sm mt-2">SOC 2 Type II certified.</p>
</div>
</div>No custom CSS file needed. The auto-rows-[180px] utility sets the row height, and col-span-2 / row-span-2 handle tile sizing.
Adding Animations
Static bento grids are fine, but subtle animations make them feel alive. Here are three levels of animation you can add.
Level 1: CSS-Only Hover Effects
Simple transforms and transitions that require zero JavaScript:
.bento-tile {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.bento-tile:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
/* Subtle inner content animation */
.bento-tile img {
transition: transform 0.3s ease;
}
.bento-tile:hover img {
transform: scale(1.05);
}Level 2: Scroll-Triggered Entrance with CSS
Use @keyframes and the animation-timeline property for scroll-driven animations — no JavaScript required:
@keyframes bento-fade-in {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.bento-tile {
animation: bento-fade-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 30%;
}
/* Stagger children */
.bento-tile:nth-child(2) { animation-delay: 50ms; }
.bento-tile:nth-child(3) { animation-delay: 100ms; }
.bento-tile:nth-child(4) { animation-delay: 150ms; }
.bento-tile:nth-child(5) { animation-delay: 200ms; }
.bento-tile:nth-child(6) { animation-delay: 250ms; }The animation-timeline: view() API is supported in all modern browsers as of 2026. It triggers the animation as the element scrolls into the viewport — purely declarative, no Intersection Observer needed.
Level 3: Framer Motion in React
For React projects, Framer Motion gives you the most control:
import { motion } from 'framer-motion';
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.08,
},
},
};
const tileVariants = {
hidden: {
opacity: 0,
y: 24,
scale: 0.96,
},
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: {
duration: 0.5,
ease: [0.25, 0.1, 0.25, 1],
},
},
};
function BentoGrid({ tiles }) {
return (
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4
auto-rows-[180px] gap-4 max-w-5xl mx-auto p-4"
variants={containerVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: '-100px' }}
>
{tiles.map((tile) => (
<motion.div
key={tile.id}
className={`bento-tile ${tile.sizeClass}`}
variants={tileVariants}
whileHover={{
y: -4,
boxShadow: '0 12px 24px rgba(0, 0, 0, 0.1)',
}}
>
<h3>{tile.title}</h3>
<p>{tile.description}</p>
</motion.div>
))}
</motion.div>
);
}The staggerChildren: 0.08 creates a wave effect as tiles animate in sequence. viewport: { once: true } ensures the animation only plays once as the grid scrolls into view.
Advanced Techniques
Named Grid Areas for Explicit Placement
When you need pixel-perfect control over which tile goes where, use named grid areas:
.bento-grid--features {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 180px);
grid-template-areas:
"hero hero stats1 stats2"
"edge edge integ integ"
"analytics security integ integ";
gap: 16px;
}
.tile-hero { grid-area: hero; }
.tile-stats1 { grid-area: stats1; }
.tile-stats2 { grid-area: stats2; }
.tile-edge { grid-area: edge; }
.tile-integ { grid-area: integ; }
.tile-analytics { grid-area: analytics; }
.tile-security { grid-area: security; }
/* Responsive: completely rearrange on mobile */
@media (max-width: 639px) {
.bento-grid--features {
grid-template-columns: 1fr;
grid-template-rows: auto;
grid-template-areas:
"hero"
"stats1"
"stats2"
"edge"
"integ"
"analytics"
"security";
}
}Named areas give you two advantages: readability (you can see the layout in the CSS) and easy responsive rearrangement (just redefine the areas).
Container Queries for Tile-Level Responsiveness
Each tile should adapt to its own size, not the viewport. Container queries make this possible:
.bento-tile {
container-type: inline-size;
}
/* When a tile is narrow (1-column), stack content vertically */
@container (max-width: 280px) {
.tile-content {
flex-direction: column;
text-align: center;
}
.tile-icon {
margin: 0 auto 12px;
}
}
/* When a tile is wide (2-column), use horizontal layout */
@container (min-width: 281px) {
.tile-content {
flex-direction: row;
align-items: center;
gap: 24px;
}
}This means a tile that is --wide on desktop and span 1 on mobile automatically adjusts its internal layout — no media queries needed.
Interactive Tiles with Hover State Content
Some bento grids reveal additional content on hover:
.bento-tile .tile-detail {
opacity: 0;
transform: translateY(8px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.bento-tile:hover .tile-detail,
.bento-tile:focus-within .tile-detail {
opacity: 1;
transform: translateY(0);
}
/* Ensure keyboard accessibility */
.bento-tile:focus-within {
outline: 2px solid #E21B1B;
outline-offset: 2px;
}Always include :focus-within alongside :hover for keyboard accessibility.
Real-World Examples and When to Use Them
Use bento grids for:
- Feature overview pages. Show 6-8 product features at a glance with varying emphasis.
- Dashboard summaries. Combine stats, charts, and status tiles in one view.
- Portfolio layouts. Mix project thumbnails at different sizes to create visual hierarchy.
- Landing page hero sections. Replace the tired single-hero-image pattern.
- Settings pages. Group related settings into visual compartments.
Avoid bento grids when:
- Content is uniform. If every item has the same importance, use a regular grid.
- Content is sequential. Blog feeds, timelines, and step-by-step flows need linear layouts.
- You have fewer than 4 items. Bento grids need density to work. Three tiles look awkward.
- Content length is unpredictable. User-generated content with varying text lengths will break fixed-height tiles.
Common Mistakes
Mistake 1: Too many tile sizes. Stick to 3-4 sizes maximum. Small (1x1), wide (2x1), tall (1x2), and large (2x2). Adding 3x1 or 3x2 tiles creates visual chaos.
Mistake 2: Inconsistent gaps. Use a single gap value. Mixing 8px and 16px gaps between different tiles looks broken.
Mistake 3: Ignoring overflow. Fixed-height tiles will clip content if you are not careful. Always set overflow: hidden and test with real content, not lorem ipsum.
Mistake 4: No dark mode. Bento tiles with borders and backgrounds need explicit dark mode styles. A white tile on a dark background with no border adjustment looks like a glitch.
/* Always define both modes */
.bento-tile {
background: #ffffff;
border: 1px solid #e5e5e5;
}
@media (prefers-color-scheme: dark) {
.bento-tile {
background: #1a1a1a;
border-color: #333;
}
}Mistake 5: Forgetting mobile. A 4-column grid on a 375px screen is unusable. Always start your responsive design from mobile up.
Performance Considerations
Bento grids are lightweight by nature — CSS Grid does not add JavaScript overhead. But tile content can be expensive:
- Lazy load images inside tiles that are below the fold.
- Use
content-visibility: autoon tiles to skip rendering offscreen content. - Avoid layout shifts by setting explicit
aspect-ratioor fixed heights on image tiles.
.bento-tile--image {
content-visibility: auto;
contain-intrinsic-size: 0 180px;
}
.bento-tile--image img {
width: 100%;
height: 100%;
object-fit: cover;
}The content-visibility: auto property tells the browser to skip rendering this tile until it is near the viewport. Combined with contain-intrinsic-size, the browser reserves the correct space to avoid layout shifts.
Wrapping Up
Bento grids work because they give designers explicit control over visual hierarchy while keeping the underlying code simple. A few grid-column: span 2 rules and you have a layout that looks custom-designed.
Start with the 4-column foundation, add 3-4 tile size variants, make it responsive at two breakpoints, and layer in subtle animations. That is all it takes.
The CSS is copy-paste ready. Go build something.