Flexbox and Grid in Practice
Compare two mental models for space and layout
Flexbox and Grid in Practice
When I wrote the HTML and CSS notes earlier, selectors, the box model, positioning, and floats all got a pass, while flexbox and grid were barely touched. That was “lesson one”, about how elements get placed into the document flow.
This lesson is about the modern layout pair: Flexbox and Grid.
They are not “replacements for the old layout” but two systems with different mental models. One starts from content and negotiates sizes along a single axis; the other starts from the page, draws a two-dimensional grid first, then fills it with content.
Get the boundary between the two models straight and most of the “which one do I use” hesitation dissolves on its own.
Hands-on versions: the Flexbox/Grid Generator, the CSS Generator, and the CSS Toolkit.
One dimension, two dimensions
Content first, layout first
Flexbox is a one-dimensional layout model. Items line up along a main axis, row or column, and how wide each one gets is negotiated between its content and the flex rules. The container supplies rules only; sizes are decided with content taking part.
Grid is two-dimensional. You declare a structure of rows and columns first, then put items into the cells. The cells exist before the content does.
Those two directions determine which problems each one is good at.
A row of buttons, a nav bar, a set of tags. Cases where the amount of content is unpredictable and the requirement is “pack them together without distorting them” are Flex’s home ground.
A whole page skeleton (header, sidebar, main, footer), an image gallery, a dashboard. Cases where what goes where is settled in advance are Grid’s home ground.
So “Flexbox or Grid” is a non-question; the real question is whether this layer of layout is driven by content or by the page.
A grown-up page almost always uses both. Grid carves the skeleton into main area and sidebar; inside the main area, card rows and toolbars are laid out with Flex.
A useful self-check: if I delete one item, should the rest renegotiate their positions (Flex semantics) or stay put and leave a hole (Grid semantics)? The answer picks the tool for you.
Two classic symptoms of a misaligned model
Both classic anti-patterns come from a misplaced model.
The first is forcing a two-dimensional page out of Flex: nav, sidebar, and main all crammed into one flex container, then three levels of nested divs and alignment properties nudged one by one. That is negotiation machinery doing declarative layout — every new module forces you to retune the flex parameters of everything before it.
The second is using Grid for rows of unpredictable content: a tag cloud as a fixed-column grid, leaving great white gaps whenever the tags run uneven. Flex’s wrapping exists precisely for content-driven arrangement like this.
Remember the symptom and the cause follows backwards.
How Flexbox works
The three-step space allocation
At Flex’s core is a space-allocation algorithm, and understanding it is worth far more than memorizing property names.
Each item has three parameters: flex-basis (where the ideal size starts), flex-grow (in what proportion surplus is split when the container has room to spare), and flex-shrink (in what proportion the shortfall is apportioned when it does not).
Allocation runs in three steps.
Set the base: give each item a preliminary size from flex-basis (default auto, i.e. falling back to the content’s size).
Share the surplus: if the flex-basis total is smaller than the container, the difference is free space and gets split by grow ratio. Items with grow 0 sit it out.
Spread the deficit: if the total exceeds the container, the overflow is charged back weighted by the product of shrink and basis, and items with shrink 0 do not shrink.
That explains two perennial confusions.
First, the difference between flex: 1 and flex: auto.
flex: 1 expands to 1 1 0%. The base collapses to zero, so the items genuinely divide the container equally.
flex: auto is 1 1 auto. The base is the content size, and only the surplus is shared evenly, so a longer item ends up wider.
Equal-width three columns use flex: 1; each taking its content size with leftovers shared uses flex: auto — one word apart, different behavior.
Second, the notorious min-width: auto trap.
A flex item’s minimum size defaults to auto, meaning no smaller than its content’s min-content. So an item holding a long URL or a long code block cannot shrink however large its shrink factor is, and it blows the row open.
The fix is an explicit min-width: 0 (or overflow: hidden alongside text). Nine times out of ten, “my flex item won’t squish” is this.
Alignment, wrapping, and what Flex lacks on purpose
justify-content on the main axis, align-items on the cross axis, plus align-self to override a single item — the three-stroke toolkit of Flex alignment.
gap adds uniform spacing between items, a generation cleaner than the old negative-margin tricks.
Another often-missed trick is margin: auto. Inside a flex container an auto margin eats all remaining space, so margin-left: auto means “push it to the end of the main axis” — the button group at the right of a nav bar goes right without an extra wrapper. margin: auto centers on both axes instead, far cleaner than positioning plus transform.
Multi-line scenarios come from flex-wrap: wrap, with align-content handling distribution between lines.
Equally worth knowing is what Flex deliberately does not have. There are no grid lines; an item cannot jump to column 3, row 2. After wrapping, each line’s height is independent, so nothing lines up across lines into cells.
That is not a missing feature but a consequence of its remit. The moment a one-dimensional negotiation model allows arbitrary jumping, it degenerates into another Grid.
The same reasoning explains the warning line around order. It changes the visual order of items, but screen readers and Tab focus still follow DOM order. Large reorderings, moving the sidebar first, should go back to DOM order or to Grid.
Axis parameters are many and the combinations abstract, so thinking them through in the air gets dizzying. Dial direction, justify, align, and wrap one combination at a time in the Flexbox/Grid Generator, read the generated code next to the visual result, and the idea of an axis welds itself shut in one sitting.
How Grid works
fr, repeat, and responsive in one line
Grid’s declarative character concentrates in track definitions.
grid-template-columns: 200px 1fr 2fr means: give the first column 200 pixels, then split what is left 1:2 between the other two. fr is a share of the remaining space, unlike a percentage (which is relative to the container’s full width, whereas fr only divides the leftovers).
The implicit grid is the safety net underneath. When items outnumber the explicitly declared tracks, Grid manufactures rows on its own, sized by grid-auto-rows — so a grid that declares only columns still grows downward.
The idiomatic one-liner for responsive card layouts:
.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px;}Word by word: each column is at least 240px and at most an equal share of the rest. auto-fit lets the column count rise and fall with the container. Wide gives three columns, narrow gives two, without a single media query.
auto-fit and auto-fill diverge when there are more column slots than cards. fill dutifully keeps the empty tracks; fit collapses them and lets the existing columns take the whole row.
Want two cards to still fill the width on a listing page, use fit; want cards never to get too wide and leave white on the right, use fill.
That one line is Grid’s expressiveness in miniature: media queries handle breakpoints, Grid handles continuous adaptation, pulling a pile of breakpoint logic that used to be hand-written back into the container’s declaration.
Grid lines, named areas, alignment vocabulary
Grid’s underlying coordinate system is grid lines. An n-column grid has n+1 column lines, numbered from 1, with -1 as the last.
An item can be placed by line (grid-column: 1 / 3 spans from line 1 to line 3) or by span (grid-column: span 2).
This is Grid’s hidden perk: DOM order and visual order decouple. Move the sidebar below the main area on small screens without touching HTML — two grid properties will do (screen readers still follow the DOM, so do not break the semantic order when rearranging).
When the page structure is stable, named areas are the most readable form:
.page { display: grid; grid-template-areas: "header header" "main aside" "footer footer"; grid-template-rows: auto 1fr auto; grid-template-columns: 1fr 280px;}.page > header { grid-area: header; } and so on. The template string is itself a to-scale sketch of the page, and reworking the page means editing this drawing.
Grid has a whole finer vocabulary for alignment, sharing names with Flex’s three-stroke toolkit but governing more.
justify-items / align-items place items inside their own cells. justify-content / align-content distribute the whole track set within the container (only meaningful when the tracks are smaller than the container). place-items: center does two lines’ work in one.
The same property name governs different objects in the two systems. In Flex, justify-content acts on items; in Grid, the same property acts on tracks. This polysemy is a side effect of the two systems borrowing property names from each other — when you use it wrong, check which model you are standing in.
Container queries and fluid scales
auto-fit solves column count following the container, but layout inside a component could still only follow the viewport. Media queries know window width and nothing else: the same card component dropped into a wide sidebar and a narrow main area has no way to sense where it is.
Container queries fill exactly that gap. Declare container-type: inline-size on the container, then use @container (min-width: 480px) { … } to switch the component’s layout on its own container’s width.
That meshes naturally with Grid’s division of labor. Grid hands out space to regions at the skeleton level, and components inside each region adapt to their new home with container queries. For the first time a component can look right wherever it lands, so cards and form rows in a design system no longer need global breakpoint special cases.
The fluid version of the same idea is clamp(). font-size: clamp(1rem, 0.5rem + 2vw, 1.5rem) slides continuously between floor and ceiling as the viewport moves. width: clamp(240px, 50%, 480px) keeps a size from ever leaving its bounds.
It and minmax() express one philosophy at different layers: declare the continuous transition between limits instead of cutting it into steps with breakpoints.
Stack the two and media queries are left with only genuine structural switches (one column to two, nav to drawer). Most width-adaptation requirements get digested locally at the component layer.
That in turn changes how components get written. Assume a component may land in a container of any width, then decide which behavior goes to container queries and which to Grid tracks. Breakpoints demoted from global constant to last resort is the most direct style migration this capability brings.
One layout, two implementations
The holy grail layout
The classic holy grail layout — header, main, sidebar, footer — in Grid is pure declaration:
/* Grid version */.page { display: grid; grid-template-areas: "header header" "main aside" "footer footer"; grid-template-rows: auto 1fr auto; grid-template-columns: 1fr 280px; min-height: 100vh;}Four items each claim a grid-area, the page is a string, and 1fr lets the main area eat the remaining height.
In Flex it takes two levels of container: an outer column for header and footer, an inner row for main and sidebar:
/* Flex version */.page { display: flex; flex-direction: column; min-height: 100vh; }.middle { display: flex; flex: 1; }.main { flex: 1; min-width: 0; }.aside { width: 280px; flex-shrink: 0; }Note that the min-width: 0 line in the Flex version is not decoration. Put a long code block in the main area and without it the row bursts — the trap from the previous section making a cameo.
The two are functionally equivalent, but Grid writes the structure in one place, the container, while Flex scatters it across two containers’ properties. If the requirement becomes “put the sidebar above the main area on medium screens”, Grid edits one line of the template string and Flex reworks the DOM hierarchy.
Conversely, once the main area holds a row of toolbar buttons of unpredictable length, Flex is plainly the more natural choice at that layer. Skeleton to Grid, content flow to Flex gets its most direct confirmation in this comparison.
The set-piece sticky-footer question works the same way. Grid declares three row tracks, auto 1fr auto, and the middle row absorbs all spare height. Flex leans on the push-to-the-bottom semantics of margin-top: auto.
Both take three to five lines. But the Grid version adds nothing to the footer itself. The layout intent is expressed entirely by the container, which is the spirit of declarative layout.
From prototype to code
Land the methodology on one concrete component: a blog listing page, two columns on desktop (post list plus sidebar), one column on mobile, with title and date at the top of each card.
The reasoning order starts at the outermost layer.
Question one: is the page skeleton one-dimensional or two? Two columns plus a footer is two-dimensional, so Grid on the container with grid-template-columns: minmax(0, 1fr) 280px. minmax(0, 1fr) rather than plain 1fr is the Grid way of preventing a burst (a 1fr track’s default minimum is also the content size, so a long code block inside a cell bursts it just the same). On mobile, switch the column template back to one column and drop the sidebar below — one breakpoint does it.
Question two lands on the card list itself. The card count is unpredictable and the width should adapt continuously to the container, so the repeat(auto-fit, minmax(240px, 1fr)) line from the previous section moves in unchanged.
Question three goes inside the card: title left, date right, a row of tags at the bottom. A one-dimensional content flow, so Flex. When titles run long, give the title container min-width: 0 plus an ellipsis.
Three layers, each in its place, under thirty lines of CSS in total, and not a single absolute position.
This reasoning order — ask the dimension, ask whether content drives it, only then write properties — transfers to almost any layout requirement.
Auto-placement and two advanced switches
Items without a grid-area get filled in by the auto-placement algorithm. The default is grid-auto-flow: row, left to right, line by line.
It has a variant worth knowing, dense. In normal mode, when a large item will not fit into the fragments left at the end of a row, it wraps and leaves a hole. dense lets later small items backtrack and fill those holes.
Masonry image walls use it to remove the gaps caused by uneven heights, at the cost of visual order drifting further from DOM order. Do not use it for lists with interactive content; a pure display gallery can consider it.
The other common switch is display: contents. It makes an element’s own box vanish so its children join the parent’s grid directly — the fix for a component wrapper breaking the grid (the extra wrapper divs a framework renders). But it erases the element’s own semantics and accessibility role, so use it only on purely structural wrappers.
At the Grid level there is also subgrid, which carries the parent’s tracks down into a nested structure and handles needs like aligning card title areas across cards. Now that support is usable, reach for it first when nested grids refuse to line up.
Five common mistakes
- Using padding as gap. The old trick of per-item padding plus negative margin on the parent distorts hover and click areas along with it. Modern browsers support
gapin both Flex and Grid; just use it. - Hard-coding the column count.
repeat(3, 1fr)throws no error and changes nothing when the window narrows — it just dies. Wherever the column count should adapt, useauto-fitwithminmax. - Expecting text-align to center things in a flex container.
text-aligngoverns inline text alignment, not the placement of flex items; centering on the main axis isjustify-content’s job. Likewise, float is ignored on flex and grid items. Muscle memory from the old layout model fails wholesale in these two systems. - Why percentage heights fail.
height: 50%needs a definite parent height, and the chain breaking anywhere turns it into auto. Grid row heights are declared at the container withfrorminmax, and most “percentage height does nothing” problems should be re-solved by allocating tracks on the container. - Treating transform as layout.
transform: translatechanges painting, not layout; the space it occupied stays where it was. Fine for nudging a centring, wrong for structural displacement — it leaves ghost space and scrollbar trouble. Structural placement belongs to the layout system.
Beyond layout
There is one more cluster of modern layout properties routinely underestimated as a naming fashion: logical properties.
margin-left and padding-top are physical properties, hard-wiring east, west, north, south. Logical properties instead use direction words relative to writing. margin-inline-start (the inline start side: the left in left-to-right Chinese, automatically the right in an Arabic interface), padding-block-end (the end of the block direction, the bottom in horizontal text).
Its relationship to the layout system only really shows on multilingual sites. One layout has to switch between Chinese, English, Japanese, and Arabic: physical properties need a whole mirrored direction override, while logical properties flip with the writing mode. Pair them with direction: rtl or writing-mode: vertical-rl and nothing needs changing.
The margin-left: auto in this lesson’s worked example should be margin-inline-start: auto in any product that supports RTL.
Building the habit of asking whether this is the inline direction or the block direction is the last step that upgrades a layout from looking like one particular language to adapting to the writing system. The layout system puts boxes into grids; logical properties stop left and right from being hard-coded.
What generators teach, and a three-step debug loop
Finally, how to use the tools so that something actually sticks.
The first level is as a cheat sheet. When you cannot recall the difference between the six alignment values, flip through them in the Flexbox/Grid Generator — visual result and matching CSS on one screen, faster than digging through docs.
The second level is as a parameter-to-effect mapper. Purely numeric parameters — shadow radius, blur and spread, corner radius, color and alpha — dragged as sliders in the CSS Generator build the mapping between number and rendering in your hands. Ten readings of the docs are worth less than ten drags of a slider.
The third level is as reading material. Tools like the CSS Toolkit emit complete styles for combination moves — glassmorphism, neumorphism, clip-path shapes. Read the generated code, annotate what each property contributes to the whole effect, then close the tool and rebuild it by hand. After those three steps the tool has produced not code but structural understanding.
The one use to guard against is treating the generator as a crutch: copy-paste every time, never read back. The generator hands you the answer; learning happens in the gap where you ask why that is the answer.
Debugging layout has its own fixed three-step loop.
Step one: switch on the devtools Grid/Flex overlay, which paints grid lines, track sizes, and axis directions onto the page, making it obvious why the layout looks the way it does.
Step two: check the box model panel to confirm whether the spacing comes from margin, padding, or gap. The three show in different colors in the overlay.
Step three: subtract. Toggle the suspect properties off one at a time and watch. When you suspect an implicit behavior like min-width: auto in particular, write an explicit min-width: 0 and compare.
The answer to a layout bug is almost always hiding in the question of which layer produced this size; the tools just visualize the question.
On a team, layout needs a few conventions, or everyone reinvents the spacing system separately.
First, spacing comes from a scale. Multiples of 4 and 8 form a finite set of steps, and gap, padding, and margin only draw from it. The tidiness of a layout is rarely mysticism; it is spacing obeying one rhythm.
Second, name layout by intent. .sidebar-layout, .card-row express intent; an implementation-detail name like .flex-gap-12 makes refactoring crawl.
Third, keep breakpoints few. Two or three across the whole site, with continuous adaptation like auto-fit absorbing the middle widths first. Every extra breakpoint grows the test matrix exponentially, and Grid can swallow most breakpoint needs anyway.
Conventions do not produce skill; they produce predictability. Open the inspector on any page and spacing and structure wear the same face.
The learning path for layout can close here. Lesson one’s box model and document flow told you what elements do on their own; this lesson’s Flex and Grid give you two declarative systems for what you want them to do.
Negotiate in one dimension with Flex, declare in two with Grid, let skeleton and content each mind their own business, and save breakpoints for genuine structural changes. The rest of the fluency hides in the dozens of layouts you tear down and rebuild by hand.