A deep, practical breakdown of CSS Grid and Flexbox — mental models, every property that matters, real-world patterns, performance notes, and the exact rules for choosing one over the other.
Why This Debate Still Exists
CSS Grid shipped in every major browser in 2017. Flexbox shipped in 2013. Almost a decade later, developers still ask which one they should be using — and the answer is still "it depends." But "it depends" is only useful if you understand what it depends on.
Most articles get this wrong. They tell you Flexbox is for "components" and Grid is for "layouts," then call it a day. That rule works maybe 70% of the time. The other 30% is where real projects live — forms that align labels and inputs across rows, cards whose internals need to stretch to the tallest sibling, dashboards where the sidebar needs to collapse at a specific breakpoint without a media query.
This guide is the version I wish I had when I was trying to stop reaching for Flexbox by reflex. We'll cover the mental models, every property that matters, the real-world patterns, the footguns, and the decision framework I use on every new project in 2026.
The Core Mental Model: 1D vs 2D
If you only remember one thing from this article, make it this:
Flexbox lays items out along a single axis. Grid lays items out along two axes at the same time.
Flexbox is a line of items. You decide the direction (row or column), and Flexbox distributes space along that line. When items don't fit, they either shrink, grow, or wrap onto a new line — but each line is still one-dimensional.
Grid is a table of cells. You define rows and columns up front, and items snap into those cells. Grid can control alignment across both dimensions simultaneously, which is something Flexbox physically cannot do without hacks.
This distinction drives almost every practical decision. A horizontal navigation bar? One dimension. Flexbox. A photo gallery where every row has to line up with every column? Two dimensions. Grid. A sidebar + main content layout where the sidebar has to match the main column's height and both have to align to a header and footer? Two dimensions. Grid.
The "Content vs Layout" Shorthand
A useful follow-up rule: Flexbox works from the content outward. Grid works from the layout inward.
With Flexbox, you hand it a bunch of items and say "figure out how to arrange these." The items decide how much space they need, and Flexbox distributes whatever is left. Add or remove an item, and the layout reshapes automatically. This makes Flexbox ideal when you don't know the number of items ahead of time.
With Grid, you define the tracks first — "I want three columns of equal width and two rows, 200px tall each." Then you place items into that structure. The layout exists before the content does, which is why Grid is better for page-level scaffolding where you care about explicit positioning.
Neither approach is better. They're just different entry points into the same problem.
Browser Support in 2026
Both are rock solid. Flexbox has been baseline since 2015 — you can use every property without a second thought. Grid has been baseline since 2017 and is supported in every browser you will realistically encounter, including every iOS Safari version people still run.
The only features that still require care in 2026 are:
subgrid— universal support since early 2023 (Safari, Chrome, Firefox, Edge). Safe to use on the public web. If you support ancient enterprise browsers, check Can I Use.- Masonry layout via Grid — still experimental at the time of writing. Firefox supports it behind a flag. Do not ship it to production yet.
align-contenton Flexbox single-line containers — changed behavior in the L2 spec; shipped in Chrome 123 and Safari 17.6. Old browsers ignore it silently, which is usually fine.
Flexbox Deep Dive
Flexbox has two sets of properties: the ones you put on the container (the parent) and the ones you put on the items (the children). Confusing them is the most common source of "why isn't this working" questions on Stack Overflow.
Container Properties
Here is the minimum set of container properties you need to have memorized.
.container {
display: flex; / or inline-flex /
flex-direction: row; / row | row-reverse | column | column-reverse /
flex-wrap: nowrap; / nowrap | wrap | wrap-reverse /
justify-content: flex-start;/ main axis alignment /
align-items: stretch; / cross axis alignment /
align-content: stretch; / multi-line cross axis alignment /
gap: 0; / space between items, row and column /
}
flex-direction sets the main axis. In row, the main axis is horizontal and the cross axis is vertical. In column, they swap. This matters because justify-content always targets the main axis and align-items always targets the cross axis — if your flex-direction changes, their meaning changes too.
flex-wrap controls what happens when items don't fit. The default is nowrap, which forces items to shrink. Switch to wrap and items that can't fit move to a new line. Almost every responsive Flexbox layout you write should use wrap.
justify-content distributes extra space along the main axis. The useful values:
flex-start— pack items at the start (default)flex-end— pack items at the endcenter— pack items in the middlespace-between— first item at start, last at end, equal gaps betweenspace-around— equal space around each item (half-sized gaps at the edges)space-evenly— equal space everywhere, including edges
align-items aligns items along the cross axis. stretch (default) makes all items the same size in that direction — this is why your flex children all end up the same height in a row layout unless you override it. The other values work the same as justify-content but on the cross axis.
gap is the property you should be reaching for instead of margins. It adds space between items without adding space at the container edges. This is so much cleaner than the old margin-right + :last-child pattern that there is no good reason to write that code anymore.
Item Properties
.item {
flex-grow: 0; / how much the item grows if there is extra space /
flex-shrink: 1; / how much it shrinks if there is not enough space /
flex-basis: auto; / the starting size before grow/shrink /
flex: 0 1 auto; / shorthand for the three above /
order: 0; / reorder without changing HTML /
align-self: auto; / override align-items for a single item /
}
The flex shorthand is the one you should actually learn. Three common values cover 90% of cases:
flex: 1— grow to fill available space, shrink if needed, ignore intrinsic size. This is the "fill the rest of the row" value.flex: none— don't grow, don't shrink, use intrinsic size. Use this for fixed sidebars or icons that shouldn't squish.flex: 0 0 300px— don't grow, don't shrink, start at exactly 300px. Use this for fixed-width panels.
flex: auto, which expands to 1 1 auto — grow and shrink, but based on content size, not available space. It gives you "soft" sizing that respects what the item naturally wants to be. Useful for nav bars where you want each item to take as much room as its label needs, but still fill the row.
The Flexbox Footguns
A few things that trip up almost everyone:
- Percentages on flex items are tricky.
flex-basis: 50%does not always mean "half the container" because grow and shrink can override it. If you need exact halves, useflex: 1 1 0on both items and let flex-grow do the math. min-width: 0saves you. Flex items have an implicitmin-width: auto, which means they refuse to shrink below their content's intrinsic size. This breaks layouts with long words orwhite-space: nowrap. Addmin-width: 0to the flex item and it will shrink properly.flex-basis: 0vsflex-basis: 0%. These behave differently in edge cases involving auto-sized containers. Prefer0unless you have a specific reason for0%.- Nested flex containers inherit nothing. If you wrap a flex container in another flex container, the inner one does not magically know about the outer one's axis. You have to set
flex-directionagain.
Grid Deep Dive
Grid has more properties than Flexbox because it's solving a harder problem. The good news is that 80% of real-world Grid usage comes down to three properties: grid-template-columns, gap, and the occasional grid-column on a specific item.
The Essential Container Properties
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr); / three equal columns /
grid-template-rows: auto; / rows size to content /
gap: 1rem; / both row and column gap /
justify-items: stretch; / horizontal alignment of items inside cells /
align-items: stretch; / vertical alignment of items inside cells /
justify-content: start; / horizontal alignment of the entire grid /
align-content: start; / vertical alignment of the entire grid /
}
grid-template-columns is where most of the magic happens. You can write it a dozen different ways:
/ Fixed columns /
grid-template-columns: 200px 1fr 200px;
/ Equal columns / grid-template-columns: repeat(3, 1fr);
/ Responsive columns without media queries / grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
/ Named lines for complex layouts / grid-template-columns: [sidebar-start] 250px [sidebar-end main-start] 1fr [main-end];
The fr unit is critical. It means "one fraction of the available space after fixed sizes are subtracted." 1fr 2fr 1fr creates three columns where the middle one is twice as wide as the outer two.
The One Line That Changes Everything
If you only learn one Grid pattern, learn this:
.responsive-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
This creates a responsive card grid that automatically fits as many 280px+ cards per row as possible, then stretches them to fill the row. No media queries, no JavaScript, no breakpoints. It works from 320px mobile screens to 4K monitors.
auto-fill vs auto-fit is the subtle bit:
auto-fitcollapses empty tracks, stretching existing items to fill the rowauto-fillkeeps empty tracks reserved, so items stay at their minmax minimum
auto-fit is what you want. If you're building something that needs consistent item widths regardless of how many items exist, use auto-fill.
Grid Areas — Layouts That Read Like ASCII Art
.layout {
display: grid;
grid-template-columns: 200px 1fr 200px;
grid-template-rows: 80px 1fr 60px;
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
min-height: 100vh;
gap: 1rem;
}
.header { grid-area: header; } .sidebar { grid-area: sidebar; } .main { grid-area: main; } .aside { grid-area: aside; } .footer { grid-area: footer; }
This is the "holy grail" layout in nine lines. You can literally see the layout by reading the template string. To change the layout at a breakpoint, you redefine grid-template-areas in a media query and items snap to their new positions with zero other changes.
Placing Items Explicitly
Sometimes you need one item to span multiple cells.
.hero {
grid-column: 1 / -1; / span from the first line to the last /
grid-row: 1 / 3; / span rows 1 and 2 /
}
.featured { grid-column: span 2; / span 2 columns from wherever the item is placed / }
The -1 trick — "end at the last line" — is extremely handy for full-width elements inside a structured grid.
Subgrid
Subgrid lets a nested grid inherit tracks from its parent. This solves one of the most annoying problems in card layouts: when you want every card's headline, body, and footer to align with the equivalent sections in every other card, regardless of content length.
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
}
.card { display: grid; grid-template-rows: subgrid; / inherits rows from .cards / grid-row: span 3; }
Before subgrid, you had to fake this with fixed heights or JavaScript measurements. Now it just works.
Real-World Patterns
Rules are fine. Patterns are better. Here are the ones I use constantly.
Pattern 1: Navigation Bar
.nav {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 2rem;
}
.nav-links { display: flex; gap: 1.5rem; }
Flexbox, one dimension, content-driven. Logo on the left, links on the right, everything vertically centered. Classic.
Pattern 2: Card Grid
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
Grid, two dimensions, layout-driven. Responsive by default. This is the one you will write a thousand times.
Pattern 3: Sidebar Layout
.app {
display: grid;
grid-template-columns: 260px 1fr;
min-height: 100vh;
}
Grid. You could do this with Flexbox, but Grid is cleaner because the sidebar width is explicit and the main column always fills the rest. If you want the sidebar to collapse on narrow screens:
@media (max-width: 768px) {
.app {
grid-template-columns: 1fr;
}
}
Pattern 4: Form with Aligned Labels
.form {
display: grid;
grid-template-columns: auto 1fr;
gap: 1rem 1.5rem;
align-items: center;
}
Labels auto-size to the widest one, inputs fill the rest. Every row lines up perfectly. Try doing this with Flexbox and you'll end up fighting yourself.
Pattern 5: Stacked Layout With Header and Footer
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.page-main { flex: 1; }
Flexbox wins here because you only care about one axis (vertical) and you want the middle section to absorb all available space. The flex: 1 on the main element is the key — it grows to fill whatever the header and footer don't use.
Pattern 6: Perfect Centering
/ Flexbox version /
.center {
display: flex;
justify-content: center;
align-items: center;
}
/ Grid version / .center { display: grid; place-items: center; }
Both work. Grid's place-items: center is shorter. Flexbox is marginally more common in existing codebases. Pick whichever your team reads faster.
Combining Both (Where It Gets Fun)
Here's the secret most beginners miss: you don't have to choose. In a real project you use Grid for the page scaffolding and Flexbox for the component internals.
.page {
display: grid;
grid-template-columns: 250px 1fr;
grid-template-rows: 60px 1fr;
grid-template-areas:
"header header"
"sidebar main";
}
.header { grid-area: header; display: flex; align-items: center; justify-content: space-between; padding: 0 1.5rem; }
.card { display: flex; flex-direction: column; gap: 0.75rem; }
.card-footer { margin-top: auto; display: flex; justify-content: space-between; align-items: center; }
The outer grid positions the three major regions. The header uses Flexbox to distribute its inline contents. The card uses Flexbox in column mode, with margin-top: auto on the footer to push it to the bottom regardless of card content. None of these techniques fight each other because they operate at different levels.
Performance — Does It Actually Matter?
In 99% of projects, no. Both Flexbox and Grid are implemented in native C++ inside the browser's layout engine. They are orders of magnitude faster than anything you could write in JavaScript.
That said, there are a few edge cases where layout cost becomes measurable:
- Huge Flexbox containers that trigger re-layout on every child change. If you have a flex container with 10,000 children and you're adding or removing them in quick succession, you might notice frame drops. Virtualized lists (React Virtual, TanStack Virtual) avoid this entirely.
- Grid layouts with complex
autosizing. Grid withgrid-template-columns: repeat(auto-fit, minmax(...))has to do more math than a grid with fixed columns. For static layouts this is irrelevant. For layouts where the grid recalculates on every scroll, it can add up. - Deeply nested flex containers. Every level of nesting adds a layout pass. Five levels is fine. Fifteen levels is asking for trouble — not because flex is slow, but because everything is slow when you nest fifteen levels of anything.
Accessibility Notes
Two things to keep in mind:
1. order and flex-direction: row-reverse break the reading order for screen readers. Visual order and DOM order should match. If you reorder with CSS, assistive technology still reads the DOM order, which is confusing. Use order sparingly and only when you're sure it won't confuse keyboard navigation.
2. Grid's auto-placement can create the same problem. If you use grid-auto-flow: dense, items get packed into the earliest available cell, which may change their visual order relative to the DOM. Same warning applies.
The rule of thumb: if a sighted user and a keyboard user would experience the content in different orders, your layout is probably lying about the structure.
The Decision Framework
Here is the exact checklist I run through when starting any new layout.
Start with Grid if:
- You need to align items across two axes at the same time
- You have a fixed number of named regions (header, sidebar, main, footer)
- You want responsive behavior without media queries using
auto-fit+minmax - You need overlapping elements (Grid handles this; Flexbox needs
position: absolute) - You want form rows where labels line up across every row
- You're laying out a single row or column of items
- The number of items is dynamic and unknown at build time
- Content-driven sizing matters more than structural precision
- You want a sticky footer on a page (flex column +
flex: 1on main) - You're arranging items inside a component (nav, card, toolbar, button group)
- Your outer layout is structured and your inner components are content-driven
- You have a grid of cards where each card is internally a flex column
- You have a grid layout with a header that needs Flexbox-style space-between
Common Mistakes I Still See in 2026
- Using Grid for a simple horizontal nav. Flexbox is shorter and reads better. Save Grid for when you need it.
- Not using
minmax()in Grid.minmax(250px, 1fr)is the single most useful Grid function and half the Grid tutorials out there never mention it. - Margin hacks instead of
gap.margin-righton every child except the last is legacy code. Usegap. It works in both Flexbox and Grid now. - Forgetting that
flex: 1collapses basis to 0. If you want "fill remaining space plus the content's natural size," useflex: autoinstead. - Nesting Flexbox to fake Grid. If you catch yourself writing a
.rowcontaining three.col-4divs, you're rebuilding Bootstrap on top of Flexbox when Grid would do the same thing in three lines. - Ignoring subgrid. It solved one of the most painful alignment problems in CSS and many developers still don't know it exists.
TL;DR
- Flexbox is one-dimensional. Grid is two-dimensional. That single fact settles most debates.
- Flexbox is content-first. Grid is layout-first. Use whichever matches your mental model for the current problem.
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))is the most useful line of CSS in the last decade. Memorize it.gapworks in both. Stop using margin hacks.- Use Grid for the page scaffold. Use Flexbox for the component internals. Combining them is not cheating — it's the correct answer.
- If Flexbox code starts fighting you, that's your cue to switch to Grid. If Grid code feels overkill, drop back to Flexbox.
- Both are performant, accessible (if you respect DOM order), and supported everywhere that matters in 2026.