You've got a row of cards and you're staring at your CSS. Flexbox or grid? You try one, it almost works, you add a few more lines, and now it looks right but you couldn't say why. Today I'll give you the one question that settles it, and we'll run both on the same real layouts so you can watch the difference yourself.

Here's the short answer, so you have it before anything else. Use flexbox when the content should decide the size of things, like a row of tags or a navigation bar. Use grid when the layout should decide the size of things, like a wall of cards or a whole page. Most real pages use both: grid outside, flexbox inside.

That's the whole idea. The rest of this article is you seeing it with your own eyes. Every example below is live, so click the buttons. If you're a beginner, read it top to bottom. If you already use both every day, jump to the section about content that won't shrink, because that's the bug that costs people whole afternoons. The later sections on keyboard order, subgrid and checking AI-written layouts are for those of you who review other people's code.

Why this choice feels harder than it should

Look, there's a reason you're confused, and it isn't you. Most material teaches these two as a list of properties. Here's justify-content, here's align-items, here's grid-template-columns, now make these colored boxes move around. You finish the exercise, and then you open a real project and nobody told you which tool goes where. It's the same thing I complain about in why courses leave you feeling like you know nothing: you get the pieces, but nobody connects them to a real page.

I've got my own version of this. For a long time I built things from my head until they looked good. No plan, no sketch. That works right up until it doesn't, and with layout it stops working the moment the content changes. You add one more card, or someone's name is longer than you expected, and the page falls apart, because you never decided how it was supposed to behave. You only decided how it should look today.

So let's fix the actual problem. You don't need to memorize more properties. You need to know what each tool is for, and then the properties mostly make sense on their own.

Note: Terms in plain words. A container is the parent element you put display: flex or display: grid on. Items are its direct children, and only direct children: grandchildren don't take part. Gap is the space between items. A track is one row or one column of a grid. Viewport is the visible area of the browser window.

The one question: who decides the size?

Picture a park bench. People sit down one after another, and each person takes up as much room as they need. A big guy in a winter coat takes more, a kid takes less. When the bench is full, the next person goes to the next bench, and the people on the second bench don't line up with the people on the first one. Why would they? Nobody planned any of it. The bench just fits whoever showed up.

That's flexbox. The items come first, and the layout forms around them.

Now picture an egg carton. The slots exist before there's a single egg in it. Every egg goes into a slot, the slots line up perfectly in rows and columns, and if you only have seven eggs, you've got seven full slots and some empty ones. A small egg doesn't make its slot smaller.

That's grid. The layout comes first, and the items go into it.

MDN puts it in almost the same terms: flexbox works from the content out, and grid works from the layout in. You'll also hear the older rule, and it's true too: flexbox is one-dimensional, it thinks about one row or one column at a time, while grid is two-dimensional, it thinks about rows and columns together. I covered that in the CSS Grid guide. But that rule alone can trick you. A wrapped row of tags looks two-dimensional on screen and it's still a flexbox job. Three equal columns in a single row look one-dimensional and grid may still be the better tool. So when you're stuck, ask the other question: should the size of this thing come from what's inside it, or from the layout around it?

Tags: the content decides, so flexbox wins

A list of tags under an article. The labels have different lengths, and you want each pill to be exactly as wide as its word. Same HTML below, two layouts. It starts on flexbox. Then click Grid and watch what happens.

Live demo: Tags: same HTML, flexbox or grid. It runs right in the page, so try it in the original article.

On flexbox it looks right, and it took very little code. On grid, every tag is forced into a column of the same width. The short ones float in too much space, "Web performance" gets squeezed onto two lines, which makes its whole row taller, and "Authentication" doesn't even fit inside its pill. Nothing is broken, it's just the wrong tool. You've got words of different lengths and you're putting them in an egg carton.

Now the code, line by line. Open the CSS tab if you want to follow along.

  • display: flex on the list makes its children sit next to each other in a row. That's the only line that really changes the layout mode.
  • flex-wrap: wrap allows a new line when the row is full. Take it away and the browser tries to keep everything on one line: the tags shrink as far as their text allows, and after that they stick out of the container.
  • gap: 8px puts space between items. It's there instead of margins, because a margin on each tag would also add space you don't want at the start or the end of a line.
  • Notice what's missing: no width anywhere. Each tag is as wide as its text plus its padding. That's the content deciding the size.

The JavaScript does nothing clever. It swaps the class name on the list, and that's what makes this a fair comparison: same HTML, only the CSS rule changes. The grid line will make sense in the next example, so don't worry about it yet.

Cards: the layout decides, so grid wins

Now the opposite. A team page with seven people on it. You want a neat wall of cards, and you want it to adapt to the screen. Same two buttons, same HTML. This time look at the last row.

Live demo: Cards: same HTML, flexbox or grid. It runs right in the page, so try it in the original article.

See it? On flexbox the last row doesn't match the rows above it. With a frame around 700 pixels wide, the top cards are 158 pixels and the bottom three stretch to 215. On a phone-sized frame the lonely last card takes the full width, double the others. Click Grid and every card is the same size, with an empty space where the missing cards would go. That's the egg carton doing its job.

So why does flexbox do this? The line flex: 1 1 140px is three numbers in one. The first means "you may grow if there's free space". The second means "you may shrink if there isn't". The third is the starting size. Each line on the bench shares out its own free space, and it has no idea what the line above it did. So a line with three cards gives each card more room than a line with four. Flexbox is doing exactly what you asked. You just asked the wrong thing.

And here's the grid line that fixes it, piece by piece, because this one line is worth knowing by heart:

  • grid-template-columns says what the columns are. The cards don't get a vote.
  • minmax(140px, 1fr) means each column is at least 140 pixels and can grow to one equal share of the leftover space. The fr unit is exactly that, a share.
  • repeat(auto-fill, ...) means make as many of those columns as fit in the available width. Wide screen, more columns. Narrow screen, fewer columns.
  • Notice what you didn't write: no media query. The layout adapts on its own, because you described the rule and not the result.

You'll also see auto-fit in the same spot, and the difference is small but real. Both make as many columns as fit, but auto-fit then collapses the empty ones. So if you only have two cards in that same 700-pixel frame, auto-fill keeps them at 158 pixels and leaves the empty columns standing, while auto-fit lets those two cards stretch to 328 pixels each and fill the row. Pick auto-fill when you want cards to stay the same size no matter how many there are. Pick auto-fit when you want a few items to fill the space.

Tip: Can you fix the last row in flexbox? Sort of. You can remove the growing with flex: 0 1 140px, but then the rows stop filling the container. Or you can calculate exact widths with calc() for every screen size. The moment you're doing that, you're rebuilding grid by hand, badly. That's your signal to switch.

A navigation bar is where flexbox feels at home

A logo, a couple of links, and a login button pushed to the far right. Every site has one. The links should be as wide as their words, and you don't want to count columns. So: flexbox.

Live demo: A navigation bar with flexbox. It runs right in the page, so try it in the original article.

Most of this you've seen. Here's what's new:

  • align-items: center lines everything up on the middle of the row. Without it, items stretch to the height of the row and their text sits at the top, so a bigger logo and smaller links don't line up.
  • margin-left: auto on the login link is the trick of the whole example. An auto margin takes all the free space on that side. So the free space goes between About and Log in, and the button lands on the far right.
  • flex-wrap: wrap is the Plan B. On a very narrow screen the button drops to a new line instead of pushing out of the page, and it stays on the right because the auto margin still works there.

So that's all it is. One auto margin. A lot of people reach for justify-content: space-between here, and it looks fine with two items. Add two more links and it spreads all of them evenly across the bar, which isn't what you wanted. The auto margin says exactly what you mean: this one item goes to the far end.

Could you do this with grid? Yes. Something like grid-template-columns: auto auto auto 1fr, with the button aligned to the end of the last column. It works, right until someone adds a fourth link and the button lands in the wrong column, because you counted the items in your CSS. When the number of things can change, and each one sizes itself, you want the bench.

Real components use both: grid outside, flexbox inside

Here's the reason "flexbox or grid" is a slightly wrong question: real components use both, each at its own level. Look at three cards with different amounts of text. The links at the bottom start out uneven, which you've seen on plenty of websites. Click the button.

Live demo: Grid outside, flexbox inside. It runs right in the page, so try it in the original article.

Two things are happening, on two levels.

The outer level is grid. The cards container uses repeat(auto-fit, minmax(170px, 1fr)), which you already know. There's a quiet bonus here: items in the same grid row stretch to the same height by default. That's why all three cards are the same height even though the text isn't. You didn't write any height at all.

The inner level is flexbox. Each card has display: flex and flex-direction: column, which stacks its title, text and link top to bottom. Now each card is a small column of things with free space at the bottom. And the same auto margin trick from the navbar works vertically: margin-top: auto on the link eats all the free space above it, and the link drops to the bottom of the card. One line.

If you're on a phone, the cards stack in one column, each only as tall as its own text. There's no free space and nothing to line up, so the button won't change anything you can see. That's correct. Layouts should behave differently when the job is different. On a bigger screen it's the difference between a page that looks finished and one that doesn't.

So when you look at a real design, stop asking which of the two to use for the page. Go level by level. The wall of cards: who decides the size? The layout, so grid. Inside a card: the content is stacked and sized by what it holds, so flexbox. Every level gets its own answer.

Whole pages: draw the layout, then let it change

A page with a header, main content, a sidebar and a footer is the most layout-decides thing there is, and grid has a feature that reads almost like a drawing. Drag the slider to pretend the screen is getting smaller.

Live demo: A page layout with grid areas (drag the slider). It runs right in the page, so try it in the original article.

Read grid-template-areas in the CSS tab like a little picture of the page. Each quoted line is a row, each word is a column. Every part of the page then claims its name: grid-area: header, grid-area: main and so on. Want the sidebar somewhere else? You change the picture, and you don't touch the HTML. If you remove the grid-area lines, the picture stops meaning anything and the items just fill the grid in order, so those lines aren't optional.

The slider is there so you can see the change in this small frame, and it's why the demo uses a container query: the @container rule looks at the width of the box around it, instead of the whole browser window. That's what container-type: inline-size on the outer box is for. Remove it and the rule has nothing to measure, so the page stays stacked. On a real page you'd usually write the same thing with @media (min-width: 768px), and the inside doesn't change at all. The rule is the same either way. Start with the phone layout, one column, then add the bigger layout when there's room for it.

If you're just starting out, that's plenty for page layouts. If you want to place items by exact rows and columns, or build something like a magazine front page, the CSS Grid guide goes much further. And if the words header, main and aside are new to you, read the HTML and CSS beginners article first, because layout sits on top of good HTML.

The bug that makes people hate both: content that won't shrink

This one gets everyone sooner or later. You've got a sidebar and a main area, you've written the layout correctly, and then a long web address or a code block shows up and the whole page gets a sideways scrollbar. You stare at it. Your CSS says the main area takes the remaining space, so why is it wider than the screen? Here it is, both versions, on purpose:

Live demo: The overflow bug and its one-line fix. It runs right in the page, so try it in the original article.

The main area burst straight through the dashed frame. Click the button and the long line gets its own little scrollbar, which is what you wanted in the first place.

Why does it happen? It's the same reason in both tools. By default, a flex or grid item refuses to get smaller than its content. The browser calls this the automatic minimum size, and it's in both specs: flexbox and grid. The default exists for a good reason, because it stops your text from being crushed into something unreadable. But a long line with no spaces can't wrap, so its minimum width is its entire length, and the item grows to fit it. It doesn't matter that you wrote overflow-x: auto on the inner pre element. That element never gets squeezed, because its parent keeps growing to hold it.

And 1fr doesn't save you, even though it sounds like it should. Written alone, 1fr secretly means minmax(auto, 1fr), and that auto is the same "not smaller than my content" rule.

So the fix is to give the item permission to be small:

  • In grid: grid-template-columns: 80px minmax(0, 1fr). The zero replaces the automatic minimum.
  • In flexbox: min-width: 0 on the item that needs to shrink. It's the same idea in a different place.
  • Either way, something inside still has to handle the overflow, and in this demo that's overflow-x: auto on the pre. Without it, the line just sticks out of its now-smaller box.

Warning: Advanced: this bug nests. If a flex item sits inside another flex item, every level between the long content and the top of the layout can need its own min-width: 0, because one level with the automatic minimum still grows. When the fix seems not to work, select the elements in your browser's dev tools going upward from the content, and find the first one wider than you expect. That's where it goes.

Keep this one in mind when you test. If a layout has only ever seen your neat sample text, you haven't tested it. Put in a long word, a long email address, or a long code line, and see what it does.

order and reversing look fine and quietly break keyboards

Both tools let you show things in a different order than the HTML. It's tempting, because it's so easy. Click into the frame below and press the Tab key a few times.

Live demo: Press Tab through these links. It runs right in the page, so try it in the original article.

The red outline jumps from right to left, backwards through what you see, because the keyboard follows the HTML and not the screen. The order property only moves the picture. A screen reader will read the HTML order too. The grid spec is direct about this: use these features only for visual reordering, never for logical reordering of content, and MDN warns about the same disconnect. WCAG has success criteria for this, on focus order and on meaningful sequence.

Grid placement does the same thing, by the way, so a pretty grid-template-areas picture can scramble the reading order too. Look back at the page layout above: the main content comes before the sidebar in the HTML and on the screen, in both layouts, so the order is honest on purpose.

The Plan B is simple. If the order matters for understanding, or if there's something you can focus on in there, change the HTML and don't fake it in CSS. Use order for things like which decorative tile sits where, where the order carries no meaning. Then test it: put your mouse away and walk through the page with the Tab key. It takes less than a minute and you'll find things.

For the advanced readers: subgrid, and the question I can't answer

Remember the three cards? We lined up their links with an auto margin. But what if the titles have different lengths too, and you want the text in all three cards to start on the same line? The inner parts of separate cards don't know about each other. That's what subgrid is for: a nested grid takes its tracks from its parent instead of making its own. MDN shows it as widely available across browsers since September 2023.

cards.css

.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } .card { display: grid; grid-row: span 3; grid-template-rows: subgrid; gap: 8px; } 
  • grid-row: span 3 makes each card occupy three rows of the outer grid, one each for title, text and link. It needs to match how many children the card really has. Add a fourth and it falls into an extra row you didn't plan for.
  • grid-template-rows: subgrid is the key line. The card stops creating its own rows and uses the parent's three. Now the tallest title in that row sets the height for all the titles.
  • display: grid on the card is required, because only a grid can have a subgrid.

With one short title and one long one, all three titles get the same row height, and all the text and all the links start on the same line. The auto margin from before only solves the last item. This solves every item.

Now the question people always ask: which one is faster? I don't have a number I trust, so I won't give you one. I wouldn't pick between them for performance on an ordinary page. Pick the one that says what you mean, because the real cost shows up later, in the person who has to change this layout in six months. That's often you. A grid with a clear picture in it is easy to change. A pile of calc() widths nobody wants to touch isn't. Thinking about the next change, not just today's screen, is the same habit behind the production-grade code checklist.

If you use Tailwind, nothing here changes

Tailwind doesn't give you a third layout system. Those classes are the same CSS with shorter names, so the question stays the same. Here's how the three patterns you just saw look in a React component:

components/layouts.tsx

export function Tags() { return ( <ul className="flex flex-wrap gap-2"> <li>CSS</li> <li>Accessibility</li> </ul> ); } export function Cards() { return ( <ul className="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] gap-4"> <li>Emily</li> <li>James</li> </ul> ); } export function Navbar() { return ( <nav className="flex flex-wrap items-center gap-4"> <a href="/">Acme</a> <a href="/login" className="ml-auto">Log in</a> </nav> ); } 
  • flex flex-wrap gap-2 is the tag row: display: flex, wrapping allowed, a small gap.
  • grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] is the card wall, written as one arbitrary value. There are no spaces inside the brackets, because a space would make it two separate classes and it would silently do nothing.
  • ml-auto is the same margin-left: auto trick. The min-w-0 class is your fix for the overflow bug.

If you're setting Tailwind up, the Tailwind v4 article walks through it. If you don't know the underlying CSS, learn that first. Otherwise you'll be typing class names for things you can't debug.

When AI writes your layout, this is how you check it

I use AI for a lot of my work, and I check what it writes as far as I can, because it makes mistakes without meaning to. Layout is a good example of why you still need the concept. AI will happily give you something that looks right in the one size you tried. It has no idea what's in your head. When the result isn't what I imagined, it's usually because I could have asked better, and now you know what asking better sounds like: "A wall of cards, minimum 14rem each, using grid with auto-fill. Inside each card, a flex column with the link pushed to the bottom. No fixed widths." That's a real instruction, and it only exists because you understand the two tools.

Then don't trust it, test it. This is the routine, and it works on layouts you wrote yourself too:

  • Ask who decides the size, and see if the code agrees. Flexbox with calculated percentage widths on a card wall is the first thing to question.
  • Add a card and remove a card. Does the last row still look intended?
  • Paste in a long word or a long web address. Does the page get a sideways scrollbar?
  • Shrink the window down to a phone width, about 320 pixels. Does anything stick out?
  • Put the mouse away and press Tab. Does the focus move in the order you read?

Five small checks, and they catch every bug in this article. If a layout is important enough to protect, ordinary unit tests won't do it, because they don't really draw the page. You need a test that runs in a real browser, and the React testing article explains where that kind of test is worth its cost.

The decision rule, and what to do next

Here's the whole article as something you can actually use. Go level by level through your design, and for each level:

  • Choose flexbox when the items should size themselves: tags, buttons, navigation bars, toolbars, a form row, or stacking things inside a card.
  • Choose grid when the layout should size the items: card walls, galleries, whole page structure, or anything that has to line up in both directions.
  • Choose neither when normal page flow already does the job. A heading above a paragraph doesn't need a layout tool. Blocks stack on their own.
  • If you're writing calc() widths or counting items in your CSS, you've probably picked the wrong one. Try the other tool before adding more code.

Now go do this. It takes ten minutes, and it's worth more than another tutorial. Open a website you use every day, right-click a part of it, and choose Inspect. In the dev tools of Chrome, Edge and Firefox, elements using these layouts carry a small flex or grid label next to them in the HTML view. Before you look, guess: flexbox or grid here? Then check. Sometimes you'll be wrong, and sometimes the site's author made a different call. Ask why. That's how you get the judgment that no property list gives you.

After that, build one thing on your own: a card wall with a link at the bottom of each card, and a navigation bar above it. You've now seen every line you need. If you get stuck, that's normal and it isn't a sign of anything. I learned most of this by trying it and getting it wrong first, and many people working in companies still look this stuff up. Don't give up on it. Then go deeper with the CSS Grid guide, which picks up where this leaves off.

This article first appeared on CodeFromScratch, where the live demos run right in the page. If you want the next one in your inbox, join the newsletter.