CSS grid vs flexbox: three layouts worked through

When to use CSS grid and when flexbox, shown with three real layouts, the pixel arithmetic behind fr, auto-fit and flex: 1, and common mistakes.

On this page

"Grid or flexbox?" has a short answer: grid when you are placing things in rows and columns, flexbox when you are lining things up in one direction and their size should come from their content. The longer answer is easier to see in real layouts than in rules. Below are three, each with the CSS the CSS grid and flexbox generator writes for it and the arithmetic the browser does, so you can predict the result before you open DevTools.

Layout 1: a page shell with grid areas

A header, a navigation column, the main content, a sidebar and a footer. This is two-dimensional (the header spans all columns, the side columns span one row), so it is grid's job:

.grid {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header header"
    "nav    main   aside"
    "footer footer footer";
  gap: 16px;
}
.header { grid-area: header; }
/* ...one rule per area */

How wide is `main`? fr shares out what is left after fixed tracks and gaps. In a 1,000 px container: 1,000 − 200 − 200 − 2 × 16 = 568 px. In an 800 px container, 368 px. The areas are a picture of the layout you can read in the CSS, and moving the sidebar to the left is a change to one string, not to the HTML.

On a phone you redefine the template in a media query with one column and the areas stacked: "header" "main" "nav" "aside" "footer". The HTML stays in a sensible reading order throughout, which matters for keyboard and screen-reader users.

In Tailwind v4 the same container is grid grid-cols-[200px_1fr_200px] grid-rows-[auto_1fr_auto] [grid-template-areas:'header_header_header'_'nav_main_aside'_'footer_footer_footer'] gap-4, which shows why page shells are often clearer in plain CSS.

Layout 2: a card list with no media queries

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 16px;
}

The browser fits as many 220 px columns as it can, then stretches them equally. The number of columns is floor((width + gap) / (220 + gap)):

Container widthColumnsEach card
1,000 pxfloor(1,016 / 236) = 4(1,000 − 3 × 16) / 4 = 238 px
700 pxfloor(716 / 236) = 3(700 − 2 × 16) / 3 ≈ 222.7 px
600 pxfloor(616 / 236) = 2(600 − 16) / 2 = 292 px
360 px1360 px

No breakpoints, and every card in a row is the same width, which flexbox with flex-wrap cannot guarantee: there, a last row of two cards stretches wider than the rows above unless you fix the basis and give up the stretch. auto-fit versus auto-fill only differs when there are fewer cards than columns: auto-fit lets two cards fill a wide row, auto-fill keeps empty columns so they stay card-sized.

Layout 3: a toolbar with flexbox

A logo, a search box that takes the remaining space, and two buttons. One direction, and the buttons should be as wide as their labels: flexbox.

.toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
}
.search {
  flex: 1;
}

The arithmetic: in a 600 px bar with a 100 px logo and two 80 px buttons, three 8 px gaps leave 600 − 100 − 80 − 80 − 24 = 316 px for the search box. Translate a label into German and the button grows; the search box shrinks to match. That content-driven sizing is what flexbox is for.

Two flex values do most of the work, and they are not the same:

  • `flex: 1` is 1 1 0%: items start from zero and split the space equally. Two items with 100 px and 200 px of content in a 600 px row become 300 px and 300 px.
  • `flex: auto` is 1 1 auto: items start from their content and split only the leftover. The same two become 250 px and 350 px (each gets half of the 300 px left over).

flex: 0 0 160px (the generator writes it as three numbers) is a fixed 160 px column that neither grows nor shrinks: the classic sidebar next to a flex: 1 main area.

Combining them

Most real pages use both: a grid for the page and the card list, flexbox inside each card to push the "Read more" button to the bottom (flex-direction: column on the card, margin-top: auto on the button), and flexbox for the toolbar. Grid answers "where does this box go?", flexbox answers "how do these boxes share a line?".

Common mistakes

  • An area that is not a rectangle. An L-shaped main, or a name used in two separate places, makes the whole grid-template-areas declaration invalid, and the browser silently ignores it. The generator refuses to output it and names the culprit.
  • `1fr` that overflows. 1fr means minmax(auto, 1fr): a long URL or a wide table inside stops the column from shrinking. Use minmax(0, 1fr) or min-width: 0 on the item.
  • The same mistake in flexbox. Flex items default to min-width: auto, so long text pushes the row wider than the screen. Add min-width: 0 to the item that should shrink.
  • Reordering content visually. order and row-reverse move boxes on screen but not in the Tab order. Keep the HTML in reading order.
  • Using `gap` margins twice. With gap on the container, remove the old margins on the items, or the spacing doubles at the edges.

Try it

Open the CSS grid and flexbox generator, load the "Responsive cards" preset and drag the corner of the preview: watch the columns drop from four to one exactly at the widths in the table above. Then switch to Flexbox and compare flex: 1 with flex: auto on items with different text. When the layout is done, give the cards depth with the box shadow generator.

Tools mentioned in this article

Back to the blog