CSS Grid vs Flexbox: Not a Versus — A Complete When-to-Use Guide
Grid or Flexbox? Stop picking sides. This guide shows exactly when to reach for each layout tool — with real code, real trade-offs, and zero fluff.
The Question Everyone Is Still Getting Wrong
Honestly, the "Grid vs Flexbox" framing is what's holding a lot of developers back. It's not a competition. They solve different problems, and once that clicks, you stop second-guessing every layout decision.
Flexbox is one-dimensional. It thinks in a single axis — either a row or a column. Grid is two-dimensional. It thinks in rows AND columns simultaneously. That's the whole mental model, and everything else flows from it.
Junior devs tend to reach for Flexbox on everything because they learned it first. Senior devs tend to reach for Grid on everything because they just discovered it. Neither approach is quite right. The answer is always: what shape is this layout?
When Flexbox Is Actually the Right Call
Use Flexbox when you're aligning items along a single axis and you want them to respond to their own content size. Navigation bars. Button groups. Card headers with a title on the left and an action on the right. Toolbars. These are all Flexbox situations.
The thing Flexbox does better than anything else is distribute space between items dynamically. You can't easily replicate justify-content: space-between with Grid without doing extra math. Flexbox handles that in one line.
Also, Flexbox wrapping (flex-wrap: wrap) is genuinely useful for tag clouds, chip lists, or any UI where you want items to flow naturally into new lines without you defining column counts. Grid wrap with auto-fill does something similar but with fixed column sizing logic — which is a different behaviour entirely, not a replacement.
When CSS Grid Changes Everything
Grid is for two-dimensional layout. Page skeletons, dashboard layouts, card grids, image galleries, magazine-style compositions — anything where you need to control both rows and columns at the same time.
The killer feature is grid-template-areas. You can literally name regions and position them by name. No absolute positioning hacks, no float clearing, no margin: auto gymnastics. It reads like a diagram of your page.
Grid also handles alignment in a way that feels more intentional for 2D layouts. place-items: center on a grid container centers both axes in two words. Try doing that reliably with Flexbox without knowing the exact height of the container. Why fight the tool when Grid does it natively?
Real Code: A Dashboard Layout in Grid and Tailwind
Here's a practical example — a dashboard shell with a sidebar, main content area, and header. This is exactly the kind of layout where Grid shines and Flexbox would make you miserable.
// DashboardLayout.tsx
export function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div
className="grid h-screen"
style={{
gridTemplateAreas: `
'sidebar header'
'sidebar main'
`,
gridTemplateColumns: '240px 1fr',
gridTemplateRows: '64px 1fr',
gap: '0px',
}}
>
<aside
style={{ gridArea: 'sidebar' }}
className="bg-zinc-900 border-r border-zinc-800 p-4"
>
{/* Nav items */}
</aside>
<header
style={{ gridArea: 'header' }}
className="bg-zinc-950 border-b border-zinc-800 px-6 flex items-center"
>
{/* Top bar */}
</header>
<main
style={{ gridArea: 'main' }}
className="overflow-y-auto p-6 bg-zinc-950"
>
{children}
</main>
</div>
);
}Notice the sidebar spans both rows automatically because of grid-template-areas. That's zero extra CSS. No position: fixed, no height: 100vh on the sidebar, no JavaScript. Grid just handles it. Combining this with Tailwind v4.0.2's utility classes for spacing and color keeps the component lean.
The Nesting Pattern That Actually Works
The real world answer is: use Grid for the outer structure, Flexbox for the inner components. They're not alternatives — they're layers.
Your page-level layout? Grid. The card inside that layout? Maybe Grid if it has multiple defined regions, or Flexbox if it's just stacking a few items in a column. The button row inside that card? Flexbox, almost certainly.
Think of it this way: Grid is architecture, Flexbox is interior design. You'd use different tools at each level anyway. If you're writing components for a library like Empire UI, this layered approach lets each component be self-contained without fighting its parent layout.
One thing to avoid: putting a Grid on every element because it's 'more capable.' Grid has overhead in the browser's layout engine. For a simple centered button label, display: flex; align-items: center is faster and more semantically obvious to the next developer reading it.
Responsive Layouts: Where Grid Has an Unfair Advantage
Grid's repeat(auto-fill, minmax(280px, 1fr)) is one of the most useful CSS patterns ever written. One line, no media queries, and you get a responsive card grid that fills available space, wraps cleanly, and never breaks below 280px per card.
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
padding: 32px;
}
/* Cards expand up to 1fr, collapse no smaller than 280px */
/* Zero media queries needed for the core behaviour */With Flexbox you'd need to set flex: 1 1 280px and then patch edge cases with a min-width and some media queries anyway. Not terrible, but more lines of intent for the same outcome. For component libraries and design systems — especially if you're building on top of something like glassmorphism-style cards — the Grid approach is just cleaner to maintain.
Media queries still matter for bigger layout shifts — like collapsing a sidebar on mobile. But for content grids, auto-fill with minmax eliminates most of them.
Tailwind Classes: Translating This Into Real Utility Syntax
In Tailwind v4.0.2, both Grid and Flexbox are first-class citizens. flex, grid, flex-row, flex-col, grid-cols-{n}, col-span-{n} — all there. The ergonomics are good. But the mental model still matters more than memorizing class names.
For a simple horizontal nav: flex items-center gap-4. For a three-column content grid: grid grid-cols-3 gap-6. For a card's internal layout (icon + text + action stacked vertically): flex flex-col gap-3. That's the pattern.
Where Tailwind and Grid get interesting together is with grid-cols-[repeat(auto-fill,minmax(280px,1fr))] using arbitrary value syntax. It looks verbose inline but it's identical to the CSS above and avoids the need for a custom stylesheet. Worth it for one-off responsive grids. For themes with many grid variants, extracting to a custom Tailwind plugin makes more sense.
If you're comparing layout approaches across different tooling stacks, our take on Tailwind vs CSS Modules covers how the authoring model affects layout complexity in real projects.
A Practical Decision Tree for Every Layout
Stop thinking about it abstractly. Here's the actual check: Does this layout need to control both rows and columns at the same time, or define named template areas? Grid. Is it a single axis of items — a row of buttons, a stacked column of form fields, a navigation menu? Flexbox.
Does it need to respond to content size dynamically without fixed column counts? Flexbox with wrapping, unless you also need consistent row heights, in which case Grid with auto-fill is cleaner. Are you building a page-level shell — header, sidebar, main, footer? Grid. Full stop.
One more: are you centering a single element inside a parent? Both work. display: grid; place-items: center is marginally more concise than display: flex; justify-content: center; align-items: center. Use whichever your team writes more naturally. It's not worth a code review comment.
The frameworks you're building with often point you in a direction too. If you're on Next.js with a component system — check out our Next.js vs Astro breakdown for how layout decisions factor into framework choice — you'll generally find Grid at the page level and Flexbox everywhere inside components.
FAQ
Yes, and you should. The standard pattern is Grid for the outer layout container and Flexbox for inner component alignment. A grid cell can itself be a flex container — the browser handles both layout contexts independently with no conflict.
Negligibly, for most UIs. Grid's two-dimensional calculations do cost slightly more than Flexbox's single-axis layout. In practice this only matters if you're rendering hundreds of complex nested grid containers. For typical component-level usage, the difference is undetectable.
Yes. Auto-fill with minmax has been fully supported in all major browsers since 2018. Safari, Chrome, Firefox, and Edge all handle it correctly. You don't need a polyfill for any currently-maintained browser.
Likely a missing flex-wrap: wrap or a flex-shrink: 0 issue on items that shouldn't compress. Flexbox shrinks items by default when space is tight. Set flex-shrink: 0 on fixed-size elements (icons, avatars, badges) and flex-wrap: wrap on the container if items should flow to new lines.
Use place-items: center on the grid container. This centers items on both axes regardless of container height. If you only want vertical centering, use align-items: center. No height declaration needed — Grid resolves it from the available space.
Grid works fine mobile-first. Start with a single-column grid (grid-cols-1) and add columns at breakpoints with md:grid-cols-2 lg:grid-cols-3 in Tailwind. The auto-fill + minmax pattern is arguably even better because it handles breakpoints without explicit media query values.