Skip to main content

Bento Grid Layouts: How to Build the Hottest UI Pattern of 2026

March 24, 2026

</>

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:

StrategyWhen to UseHow It Works
Collapse to single columnContent-heavy tilesAll tiles become full-width, stacked vertically
Reduce columnsVisual/stat tiles4 cols -> 2 cols -> 1 col at breakpoints
Hide non-essential tilesFeature pagesShow only primary tiles on mobile with display: none
Reorder tilesHero/CTA focusedUse 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: auto on tiles to skip rendering offscreen content.
  • Avoid layout shifts by setting explicit aspect-ratio or 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.

Recommended Posts