Flexbox vs Grid: When to Use Which CSS Layout System

Flexbox vs Grid: When to Use Which CSS Layout System

Both Flexbox and Grid are powerful, both are well-supported, and both solve layout problems. The confusion comes from not having a clear mental model for when each applies. Once you have that model, the choice becomes obvious — and using them together is where the real power is.

The Core Mental Model

Here's the simplest way to think about it: Flexbox lays things out in one direction. Grid lays things out in two directions.

Flexbox works along a single axis — either a row or a column. It's excellent at distributing space among items and aligning them relative to each other. Grid works on a two-dimensional canvas of rows and columns simultaneously. It's built for placing items into a defined structure.

The practical translation: use Flexbox for components (navigation bars, button groups, card internals, form rows), and use Grid for page-level or section-level layout (the overall page structure, gallery grids, dashboard panels).

That said, the line isn't always clean, and knowing both well lets you choose based on the actual problem rather than a rule of thumb.

Flexbox: The One-Dimensional Workhorse

Flexbox turns a container into a flex container and its direct children into flex items. The two most important properties to internalize are flex-direction and how the axes work.

.nav {
  display: flex;
  flex-direction: row;        /* default: items flow left to right */
  justify-content: space-between; /* main axis distribution */
  align-items: center;        /* cross axis alignment */
  gap: 1rem;
}

justify-content controls distribution along the main axis (the direction of flex-direction). align-items controls alignment along the cross axis (perpendicular). This is where most people get tripped up — the axes flip when you switch flex-direction to column.

flex-wrap and Implicit Rows

By default, flex items squeeze into a single line. Add flex-wrap: wrap and items spill to new lines when they don't fit:

.card-row {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.card {
  flex: 1 1 280px; /* grow, shrink, basis */
}

flex: 1 1 280px is shorthand for flex-grow: 1; flex-shrink: 1; flex-basis: 280px. Each card starts at 280px, grows to share available space, and shrinks if needed. With flex-wrap: wrap, you get a responsive row of cards without any media queries.

flex-grow, flex-shrink, flex-basis

These three sub-properties define how an item sizes itself relative to its siblings:

  • flex-grow: how much of the remaining space an item claims (0 = don't grow)
  • flex-shrink: how aggressively the item shrinks when space is tight (0 = don't shrink)
  • flex-basis: the item's starting size before growth/shrinkage is applied
/* Sidebar + main content layout */
.sidebar { flex: 0 0 240px; } /* fixed width, no grow, no shrink */
.main    { flex: 1 1 0; }     /* fills all remaining space */

Grid: The Two-Dimensional Powerhouse

Grid gives you explicit control over both rows and columns at once. You can place items precisely into named areas, span them across multiple tracks, and define structure that items automatically flow into.

.page-layout {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "sidebar header"
    "sidebar main"
    "sidebar footer";
  min-height: 100vh;
}

.sidebar { grid-area: sidebar; }
.header  { grid-area: header; }
.main    { grid-area: main; }
.footer  { grid-area: footer; }

Named areas make the layout intention readable at a glance. At mobile breakpoints you just redefine the template.

The `fr` Unit

fr stands for fraction of available space. 1fr 1fr 1fr creates three equal columns. 1fr 2fr creates two columns where the second is twice as wide as the first. It's more flexible than percentages because it accounts for gaps:

.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1rem;
}

auto-fill vs auto-fit

These two values for repeat() create responsive grids without media queries:

/* auto-fill: creates as many tracks as fit, leaves empty ones */
.grid-fill {
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}

/* auto-fit: collapses empty tracks, items stretch to fill */
.grid-fit {
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

For most card grids, auto-fit is what you want — it expands items to fill the row rather than leaving ghost columns. For grids where items shouldn't stretch (like a toolbar), auto-fill preserves consistent item sizing.

Where They Overlap

Some layouts can be done with either tool, and the right choice comes down to intent. A simple three-column equal-width layout is trivially achievable with both:

/* Grid approach */
.three-col { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }

/* Flex approach */
.three-col { display: flex; gap: 1rem; }
.three-col > * { flex: 1; }

For this case, either works. But if you later need one item to span two columns, Grid has a clean answer (grid-column: span 2) and Flexbox requires workarounds.

If you need items that reorder themselves based on intrinsic size, Flexbox's natural flow behaviour is a better starting point.

Using Both Together

The best layouts often use Grid at the outer level and Flexbox inside components. Grid establishes the structural skeleton; Flexbox handles internal component alignment.

/* Grid page structure */
.layout {
  display: grid;
  grid-template-columns: 1fr 3fr;
  gap: 2rem;
}

/* Flexbox card internals */
.card {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.card-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: auto; /* push footer to bottom */
}

margin-top: auto on a flex child pushes it to the far end of the cross axis — a classic flex trick for sticking a footer to the bottom of a variable-height card.

The `gap` Property

Both systems share the gap property (previously grid-gap for Grid). It sets spacing between items without adding margins to edges:

.grid { display: grid; gap: 1rem; }          /* same gap in both directions */
.grid { display: grid; gap: 1rem 1.5rem; }   /* row-gap column-gap */
.flex { display: flex; gap: 0.5rem; }

gap is cleaner than using margins because it doesn't create double-spacing between items or unwanted margin on the first/last item.

Subgrid

Subgrid solves a long-standing limitation: child elements couldn't participate in a parent's grid tracks. With subgrid, a grid item can inherit its parent's row or column definitions:

.parent {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

.child {
  grid-column: span 3;
  display: grid;
  grid-template-columns: subgrid; /* inherits parent's columns */
}

This is particularly useful for card grids where you want labels and values to align across cards. Subgrid is now supported in all modern browsers.

The Practical Decision

When starting a new component, ask: do I need to align things in two dimensions, or just one? Navigation bar, button group, input row, flex card — Flexbox. Page shell, dashboard layout, image gallery, grid of repeating items — Grid.

And when your CSS gets complex, keeping it lean matters. The CSS Minifier removes whitespace, redundant rules, and comments so what ships to users is as tight as possible.

For the specificity and cascade behavior that determines which layout rules actually win, CSS Specificity: The Complete Guide is essential reading. And to understand how these layout styles interact with the browser's rendering pipeline, CSS Custom Properties Explained shows how variables integrate cleanly into both Flexbox and Grid systems.