Hand-drawn diagram: CSS Grid vs Flexbox, decided by axis-count. Left to right: Page frame (CSS Grid · two axes, tracks declared) to Region contents (Flexbox · one axis, sized to content) to Card media object (Flexbox · avatar + text, one axis); grid for the frame, flex for the contents.

CSS Grid vs Flexbox, and how the two compose in real layouts

One axis or two, tracks known or flow-to-content? The rule that settles which to reach for, and the master pattern that puts both to work at once.

CSS Grid vs Flexbox: stop asking which one wins#

Most guides frame CSS Grid vs Flexbox as a contest. That framing is the mistake. The two are partners, and the browser ships them to solve different shapes of problem. So the useful question is never which tool is stronger. Instead the question is which shape you are laying out right now.

The specification says this plainly. MDN's page on the relationship of grid layout with other layout methods treats them as complementary, not competing. Therefore the rest of this post reframes the tired one-dimensional versus two-dimensional shorthand as its underlying cause. Once you name the cause, the decision stops being a matter of taste.

Grid is layout-driven: you define the tracks, then place items in#

Grid is a layout-driven system. First you declare rows and columns. Then you place items into that structure. Because the tracks exist before the content, you describe the frame up front and the browser positions each item into it. That is the real meaning of two-dimensional. You are working both axes together, on purpose.

This post defers the how of defining those tracks. Named areas, subgrid, and intrinsic sizing are a deep topic on their own. For that mechanics, read the deep-dive on CSS Grid for complex layouts. Here the point is only the decision, so we keep the focus on when Grid is the right frame.

Flexbox is content-driven: items size to content and distribute on one axis#

Flexbox is a content-driven system. Items flow along a single axis, and each one sizes to its content. Instead of pre-declaring tracks, you distribute the free space that is left over. So a flex container asks a different question. It asks how to share one line of space among items that already know how big they want to be.

MDN's Flexbox guide frames it the same way. Because the model is one axis and content-first, Flexbox shines on a nav bar, a button row, or a wrapping chip list. In each case the items lead and the layout follows.

So one-dimensional vs two-dimensional is the tell, not the test#

Here is the bridge. The axis count is a symptom you can observe. The cause is the model you actually decide on: layout-driven structure, or content-driven flow. Therefore one-dimensional versus two-dimensional is the tell, not the test. When you feel unsure, drop back to the cause and the tell resolves itself.

The one decision rule#

Every ambiguous case yields to two questions asked in order. First, are you positioning along one axis, or placing into a structure of two? Second, do you know the tracks up front, or should items flow to their content? Follow the branches and you land on a tool with a reason, not a guess.

The two-question decision rule: axis-count first, then tracks-known versus flow-to-content, routing to Grid or Flexbox.Ask axis-count first, then ask whether the tracks are known up front. Two axes with declared tracks means Grid. One axis with content-sized items means Flexbox. The edge branches show that either tool can cover a single row when the other constraint pushes it there.

Notice what the diagram does. It answers the CSS Grid vs Flexbox question by cause, not by folklore. Because the branches key off intent, the same rule works for a nav bar and for a dashboard. In short, you decide once and the layout follows.

Apply the rule: a lookup for the UI you actually build#

A rule is easier to trust when you see it applied. So here is the one-glance table the top results tend to skip. Each row is a real piece of UI. Read across to see the axis count, whether the tracks are known up front, and the tool the rule picks.

The decision rule applied to the components you ship every week
The UI you are buildingAxis countTracks known up front?Reach for
Nav barOne axisNo, size to contentFlexbox
Button rowOne axisNo, size to contentFlexbox
Form field rowOne axisNo, label then inputFlexbox
Whole-page skeletonTwo axesYes, named areasCSS Grid
Card wall or galleryTwo axesYes, repeat() tracksCSS Grid
Dashboard region gridTwo axesYes, rows and columnsCSS Grid
Media object (avatar + text)One axisNo, content flowsFlexbox
Toolbar that wrapsOne axisNo, wrap to contentFlexbox

Read the table as one rule, not eight opinions. Whenever the intent is a single row that flows to content, Flexbox wins. Whenever the intent is a structure of two axes with tracks you can name, Grid wins. That consistency is the whole payoff of deciding by cause.

The master pattern: grid-for-the-frame, flex-for-the-contents#

Now the spine of the whole page. The strongest layouts do not pick a side. Instead they use Grid for the macro frame and Flexbox for the contents inside each region and card. This is the composition that the CSS Grid vs Flexbox pairing was built for. Name it once and you will see it everywhere.

The demo below proves it live. Flip the spotlight between Frame, Contents, and Both, and watch which regions light up. Then drag the width and watch the real layout re-solve. Grid re-flows the macro frame, while Flexbox re-sizes the contents inside each card.

Grid frames the page, Flexbox flows the contents
Spotlight (which tool owns which region)
Grid: the macro frameFlex: the contents inside
grid area: header
flex navbarLogoDocsPricingSign in
grid area: sidebar
OverviewReportsSettings
grid area: main
Card 1: avatar fixed, text flows.
Card 2: avatar fixed, text flows.
Card 3: avatar fixed, text flows.
Card 4: avatar fixed, text flows.
Card 5: avatar fixed, text flows.
Card 6: avatar fixed, text flows.
grid area: footer
flex rowStatus: OKSynced 2m ago
Grid frame2 macro cols
Card wall3 per row
Card media object1 flex axis

Grid frames the page. Flex flows the contents. At 800px the grid frame holds 2 macro columns (a 320px sidebar plus a flexible main). Grid frames the page. Inside main the card wall reflows to 3 cards per row, and inside each card a flex media object keeps the avatar at a fixed size while the text sizes to what is left. Flex flows the contents.

One layout, two tools. Grid places the macro regions; flex flows the contents inside each one.
RegionToolWhy that tool
Page skeletonCSS GridTwo axes, named areas, tracks declared up front.
Card wall in mainCSS GridA two-axis wall that reflows with repeat(auto-fill, minmax()).
Navbar in headerFlexboxOne axis, items sized to content, one push to the right.
Media object in each cardFlexboxFixed avatar, text flexes to fill the remaining main-axis space.
Footer meta rowFlexboxOne axis, content-sized items with a spacer between.
Use the Frame / Contents / Both lens to spotlight which tool owns which region: the grid macro areas light in the brand accent, the flex contents light in the strong accent. Drag the container width to watch the grid frame stack below the breakpoint while the card wall reflows and each card's flex media object keeps its avatar fixed. With JavaScript off, the Both view renders at 800px with a source-of-truth table, so the lesson still lands.

Look at what each tool contributes. Grid gives the page its aligned macro regions across two axes. Flexbox gives each internal row its content-driven distribution along one axis. Because neither could do the other's job cleanly, the composition is not a compromise. It is the point.

Deciding the ambiguous cases out loud#

The rule earns its keep on the cases where both tools plausibly work. So this section walks the reasoning out loud, which is exactly what most references skip. In each case we ask the two questions and let the answer fall out. The procedure is the same every time.

The media object: grid or flex?#

For example, consider an avatar next to a block of text. Both tools can build it, so run the rule. The intent is one axis, a single row, and the avatar sizes to its content while the text takes the rest. Therefore Flexbox is the honest fit. Still, Grid works too, so compare them directly.

media-flex.css · css
/* Flexbox media object: one axis, avatar sized to content, text flows to fill.
   The intent is content-driven, so you distribute free space, not declare tracks. */
.media {
  display: flex;
  gap: 16px;
  align-items: center;
}
.media__avatar { flex: 0 0 64px; } /* fixed 64px, never grows, never shrinks */
.media__text   { flex: 1 1 auto; } /* takes whatever main-axis space is left */

Both render the same picture, yet they say different things. The flex version distributes free space along one axis, which matches a media object's intent. The grid version declares a fixed avatar column and a flexible text column, which is a small two-track frame. For a lone row, the flex reading is lighter. For a card whose internals must align with siblings, the grid reading can pay off, a theme our guide on building scalable UI components covers in depth.

Holy grail: flex navbars nested inside grid areas#

The holy grail layout is the classic proof of composition. First, Grid draws the page frame as header, sidebar, main, and footer. Then Flexbox flows the contents inside those regions. The annotated skeleton below marks which regions are the grid frame and which strips are nested flex.

  1. Header is a grid areaThe page frame is a two-axis CSS grid with named areas. The header spans the full top row of that frame.
  2. Navbar is nested flexInside the header, a flex row flows the logo, the links, and the sign-in button along one axis, each sized to its content with one push to the right.
  3. Sidebar is a grid areaThe sidebar is a named column in the grid frame. Its own nav links stack in a one-axis flex column inside it.
  4. Main is a grid areaThe main region is the flexible column of the frame. It holds a card wall, which is itself a smaller two-axis grid.
  5. Card internals are flexInside each card, a flex media object keeps the avatar at a fixed size while the text flows to fill the remaining space.
  6. Footer is a grid areaThe footer spans the full bottom row of the grid frame, and a flex row distributes its status text and timestamp.
One real layout, both tools labelled by region. The four macro regions are the grid frame. The navbar, the card media objects, and the footer meta row are flex contents nested inside those grid areas.

Read the layout as two layers. The outer layer is a grid, because a page frame is two axes with named tracks. The inner layers are flex, because each strip is one axis of content-sized items. So the holy grail layout is not a Grid demo or a Flexbox demo. It is a composition demo.

The toolbar that wraps: flex-wrap vs grid auto-fit#

A toolbar is the case that splits the room. It is one row of controls that must wrap when space runs out. Both flex-wrap and a grid auto-fit track can do it, so compare them on the same component. The difference is what happens to the last row.

toolbar-flex.css · css
/* Content-driven: each control keeps its own width and wraps when out of room.
   The last row is uneven, because the items size to content, not to a track. */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.toolbar > * { flex: 0 0 auto; } /* size to content, then wrap */

In practice, the flex-wrap version keeps each control at its content width, so the last row is uneven and left-packed. Meanwhile, the grid auto-fit version snaps the controls to equal tracks, so every row aligns. Therefore the choice is about intent again. For a toolbar whose buttons should keep their natural size, flex-wrap reads truer, while an aligned grid of equal cells wants auto-fit. The responsive trade-offs here also touch performance, which our post on Lighthouse performance audits for mobile-first optimization unpacks.

Alignment parity: where they share, where they split#

Alignment is where the two models look most alike and quietly diverge. Both honor the box-alignment properties, yet Grid gains a second axis of control that Flexbox has no use for. So keep this parity matrix beside your editor. It marks each property as shared, grid-only, or flex-only.

The box-alignment properties, marked shared, grid-only, or flex-only
PropertyWorks in GridWorks in FlexWhat it does
justify-contentYesYesDistributes free space on the inline axis of the container.
align-itemsYesYesAligns items on the cross axis within their line or row.
align-contentYesYesDistributes extra space between rows or lines when they wrap.
place-contentYesYesShorthand for align-content plus justify-content.
justify-itemsYesNo effectAligns items on the inline axis; Grid only, since Grid has that axis.
justify-selfYesNo effectAligns one item on the inline axis; Grid only.
align-selfYesYesOverrides align-items for a single item on the cross axis.

Read the split as a symptom of the models. Because Grid owns two axes, it adds inline-axis control through justify-items and justify-self. Flexbox distributes along one axis, so those properties have nothing to act on and are inert. The W3C CSS Box Alignment specification defines the exact behavior on each layout mode.

Worked example: a 1200px dashboard, to the pixel#

Now put numbers on the master pattern. Take a 1200px dashboard that uses grid-for-the-frame and flex-for-the-contents. Grid defines the card wall, and Flexbox lays out each card's internals. Work the arithmetic once and the demo above stops being magic.

dashboard-compose.css · css
/* Grid frames the card wall. In a 1200px container this resolves to 4 columns:
   5 columns need 5*280 + 4*24 = 1496px (too wide), 4 columns need
   4*280 + 3*24 = 1192px (fits), so each 1fr column is (1200 - 3*24) / 4 = 282px. */
.dashboard {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 24px;
}

/* Flex flows the contents INSIDE each 282px card as a one-axis media object.
   The text block receives 282 - 64 - 16 = 202px automatically, no width set. */
.card {
  display: flex;
  gap: 16px;
}
.card__avatar { flex: 0 0 64px; } /* fixed 64px avatar column */
.card__text   { flex: 1 1 auto; } /* sized by the remaining main-axis space */

Start with the grid. With repeat(auto-fill, minmax(280px, 1fr)) in a 1200px container, five columns would need 1496px and do not fit. Four columns need 1192px and do fit, so the browser lands on four. Consequently each 1fr column computes to (1200 minus 3 times 24) divided by 4, which is 282px.

Then move inside a card. The flex media object sets the avatar to flex: 0 0 64px and the text to flex: 1 1 auto. So the text block receives 282 minus 64 minus 16, which is 202px, with no width declared. Change the viewport and Grid re-flows the number of cards, while Flexbox re-sizes the text inside each one. Neither tool could do the other's job cleanly, which is the whole composition.

When the honest tiebreaker is either works#

Sometimes you run the rule and both tools still qualify. That is fine, and it deserves an honest answer rather than a shrug. The honest CSS Grid vs Flexbox tiebreaker is this. Pick by whether your intent is one axis or two, not by the tool you happen to know better.

In practice that heuristic resolves most ties fast. Because a single row of content-sized items is one axis, Flexbox is the calmer choice even when a one-column grid would also work. Because a structure with aligned rows and columns is two axes, Grid is the calmer choice even when nested flex could fake it. In short, let the shape decide, and move on.

When not to reach for this split#

There is one more trap to name. Do not treat this decision as permanent architecture. A section can start as a flex row and grow into a grid frame as its content gains a second axis. In the end, both tools are cheap to swap, so let the layout tell you when it has changed shape.

Where to go deeper on Grid itself#

Untangling a layout that keeps sprouting breakpoints, or a design system where cards refuse to line up? We are happy to talk through the frame-and-contents model with no pressure and no lock-in.

Talk through a frontend build

Keep reading