Flexbox — The Complete Guide
flex-direction and the main-axis vs cross-axis mental model, justify-content, align-items, align-content, flex-wrap, and flex-grow/flex-shrink/flex-basis worked through with real numeric examples.
The Main Axis and Cross Axis — The Single Most Important Idea in Flexbox
Everything in Flexbox — every property covered in this module — only makes sense once one idea is genuinely internalized: turning an element into a flex container creates two axes, and every alignment property in Flexbox refers to one of those two axes, never to literal "horizontal" or "vertical." Get this backwards and every Flexbox property will feel like it is behaving randomly; get it right and the entire spec becomes predictable.
.container {
display: flex;
/* every direct child of .container is now a "flex item" */
}The main axis is the direction items are laid out along — controlled entirely by flex-direction (Part 02). The cross axis is always exactly perpendicular to the main axis. This is the entire trick: flex-direction does not just change which way items flow, it redefines what "main" and "cross" mean for every other Flexbox property in the container.
.row {
display: flex;
flex-direction: row; /* main axis = horizontal, cross axis = vertical */
justify-content: center; /* centers items HORIZONTALLY */
align-items: center; /* centers items VERTICALLY */
}
.column {
display: flex;
flex-direction: column; /* main axis = vertical, cross axis = horizontal */
justify-content: center; /* centers items VERTICALLY — same property, different axis! */
align-items: center; /* centers items HORIZONTALLY — same property, different axis! */
}justify-content) and one is always the cross-axis property (align-items/align-content). Once flex-direction changes, which physical direction each property affects flips with it. Memorize the rule as "justify = along the main axis, align = across the cross axis" rather than "justify = horizontal, align = vertical," and the confusion disappears permanently.flex-direction — Choosing the Main Axis
.a { flex-direction: row; } /* default — main axis left to right */
.b { flex-direction: row-reverse; } /* main axis right to left */
.c { flex-direction: column; } /* main axis top to bottom */
.d { flex-direction: column-reverse; } /* main axis bottom to top */row is the default — if you never set flex-direction, a flex container lays its children out left-to-right (in a left-to-right language), with the main axis horizontal. The reverse variants flip the order items appear in visually, without changing their order in the actual HTML/DOM — an important distinction for accessibility, since screen readers and keyboard tab order still follow DOM order, not the visual reversed order.
<div style="display: flex; flex-direction: row-reverse;">
<button>First</button>
<button>Second</button>
<button>Third</button>
</div>
<!--
Visually renders: Third, Second, First
But Tab still moves focus First -> Second -> Third, because keyboard
navigation follows DOM order, not visual/painted order. A sighted
mouse user and a keyboard user can experience a genuinely different,
inconsistent sequence — this is a real accessibility consideration,
not a theoretical one.
-->row-reverse and column-reverse are used far more cautiously in production interfaces than row/column — reach for them for purely decorative reordering, and verify with an actual keyboard (not just visually) whenever the reversed content is interactive.justify-content — Distributing Space Along the Main Axis
justify-content controls how extra space is distributed along the main axis, once the flex items themselves have already taken up whatever room they need. It only has a visible effect when the items do not already fill the container completely.
.a { justify-content: flex-start; } /* default — items packed at the start */
.b { justify-content: flex-end; } /* items packed at the end */
.c { justify-content: center; } /* items packed in the middle, equal space each side */
.d { justify-content: space-between; } /* first item at start, last at end, EQUAL gaps between (none at the outer edges) */
.e { justify-content: space-around; } /* equal space AROUND each item — edge gaps are HALF the between-item gaps */
.f { justify-content: space-evenly; } /* every gap — including the two outer edges — is exactly equal */The distinction between space-between, space-around, and space-evenly is a genuinely common point of confusion worth being precise about: with three items in a container, space-between creates two gaps (only between items, none at the edges); space-around creates four gaps, but the two edge gaps are each half the size of the two between-item gaps; space-evenly creates four gaps that are all exactly equal, including the edges.
align-items and align-content — Two Different Cross-Axis Properties
align-items controls how items are positioned across the cross axis within a single line — the direct cross-axis equivalent of justify-content, but for one row (or column) of items.
.a { align-items: stretch; } /* default — items stretch to fill the container's cross-axis size */
.b { align-items: flex-start; } /* items align to the top of the cross axis */
.c { align-items: flex-end; } /* items align to the bottom */
.d { align-items: center; } /* items centered vertically */
.e { align-items: baseline; } /* items aligned by their text baseline — useful when items have different font sizes */align-items: stretch is the default, and it genuinely surprises people the first time — flex items with no explicit height, inside a row-direction flex container, automatically stretch to match the tallest sibling's height unless align-items is set to something else. This is precisely the mechanism behind the "equal-height cards" pattern covered fully in the next module, and it happens with zero extra CSS beyond display: flex itself.align-content — only relevant once items wrap onto multiple lines
align-content is easy to confuse with align-items, but it solves a different problem: it controls how entire lines of wrapped flex items are distributed across the cross axis relative to each other — it has zero visible effect with a single line of items (the overwhelmingly common case, e.g. flex-wrap: nowrap, the default). It only does anything once flex-wrap: wrap (Part 05) produces multiple lines and there is extra space in the cross-axis direction to distribute between them.
.gallery {
display: flex;
flex-wrap: wrap;
height: 600px; /* taller than the wrapped content needs, so there IS extra cross-axis space */
align-content: space-between; /* spreads the WRAPPED LINES apart from each other */
/* align-items still controls alignment WITHIN each individual line */
}flex-wrap — Allowing Items to Flow Onto New Lines
By default, every flex item is forced onto a single line (flex-wrap: nowrap), shrinking to fit if necessary (governed by flex-shrink, Part 07) rather than ever wrapping. flex-wrap: wrap allows items to flow onto additional lines once they no longer fit on the current one.
.a {
display: flex;
flex-wrap: nowrap; /* default — items squeeze/shrink to stay on one line, however cramped */
}
.b {
display: flex;
flex-wrap: wrap; /* items that no longer fit flow onto a new line instead of shrinking indefinitely */
}
.c {
display: flex;
flex-wrap: wrap-reverse; /* wraps, but new lines stack ABOVE the first line instead of below */
}flex-grow — Distributing Extra Space, Worked Numerically
flex-grow controls how leftover space along the main axis is distributed once every item has already taken its base size. Its default is 0 — meaning by default, extra space is not distributed to items at all, it simply sits unused (governed by justify-content instead). The number is not a percentage or a pixel value — it is a ratio, and understanding it requires actually doing the arithmetic once.
.container {
display: flex;
width: 900px;
}
.item-a { flex-grow: 1; width: 100px; } /* base width before growing */
.item-b { flex-grow: 2; width: 100px; }
.item-c { flex-grow: 1; width: 100px; }
/*
Total base width used: 100 + 100 + 100 = 300px
Leftover space to distribute: 900 - 300 = 600px
The three flex-grow values (1, 2, 1) sum to 4 "shares."
Each share of the leftover 600px is worth: 600 / 4 = 150px
item-a gets 1 share: 100px + (1 × 150px) = 250px final width
item-b gets 2 shares: 100px + (2 × 150px) = 400px final width
item-c gets 1 share: 100px + (1 × 150px) = 250px final width
Final check: 250 + 400 + 250 = 900px — exactly fills the container.
item-b ends up exactly TWICE as wide as item-a and item-c combined
relative to their growth, because its flex-grow ratio is exactly double.
*/The takeaway that matters: flex-grow values are only meaningful relative to each other, not as absolute numbers. flex-grow: 1 on every item produces identical results to flex-grow: 100 on every item — what matters is each item's share of the total across all items in that container, not the specific numbers chosen.
.toolbar {
display: flex;
}
.toolbar-title { flex-grow: 0; } /* stays exactly its natural size */
.toolbar-spacer { flex-grow: 1; } /* absorbs 100% of the leftover space — pushes the next item to the far end */
.toolbar-actions { flex-grow: 0; } /* stays exactly its natural size */
/* This exact three-item pattern — a fixed title, an invisible flex-grow: 1
spacer <div>, and fixed trailing action buttons — is one of the single
most common real-world Flexbox layouts, used constantly for toolbars,
headers, and any "stuff on the left, stuff pinned to the right" row. */flex-shrink — The Same Ratio Logic, Running in Reverse
flex-shrink is the mirror image of flex-grow: instead of distributing extra space, it distributes a deficit — how much each item gives up when the container is too narrow to fit every item at its base size. Its default is 1 (unlike flex-grow's default of 0) — meaning, by default, every flex item is willing to shrink.
.container {
display: flex;
width: 500px;
}
.item-a { flex-shrink: 1; width: 300px; }
.item-b { flex-shrink: 1; width: 300px; }
.item-c { flex-shrink: 1; width: 300px; }
/*
Total base width: 300 + 300 + 300 = 900px
Container is only 500px — an overflow of 400px that must be removed.
With EQUAL flex-shrink values (1, 1, 1) and equal base widths, the
400px deficit is split evenly: each item shrinks by 400 / 3 ≈ 133px
item-a: 300 - 133 = 167px
item-b: 300 - 133 = 167px
item-c: 300 - 133 = 167px (rounding — actual browser math is more precise)
*/flex-shrink: 0 on a specific item (commonly an icon or a fixed-width sidebar) prevents it from ever shrinking below its base size, letting only the other, shrink-enabled items absorb the deficit..layout {
display: flex;
}
.sidebar {
flex-shrink: 0; /* never shrinks below its own width, no matter how narrow the container gets */
width: 240px;
}
.main-content {
flex-shrink: 1; /* absorbs all of the shrinking */
flex-grow: 1; /* also expands to fill any leftover space */
}flex-basis — The Starting Size Before Grow/Shrink Are Applied
flex-basis sets an item's starting main-axis size before flex-grow or flex-shrink are applied — conceptually, "what width would this item be if grow/shrink math had not run yet." Its default value is auto, which falls back to the item's own width (in a row container) or content size if no width is set at all.
.item {
flex: 1 1 200px;
/* flex-grow: 1 — willing to grow to fill space
flex-shrink: 1 — willing to shrink (this is also the default)
flex-basis: 200px — starting size before grow/shrink math runs */
}
/* The three most common shorthand patterns you will actually see: */
.equal-width-item { flex: 1; } /* shorthand for flex: 1 1 0% — items split space perfectly equally, ignoring their content's natural size entirely */
.fixed-size-item { flex: 0 0 200px; } /* never grows, never shrinks, always exactly 200px regardless of container size */
.natural-then-grow { flex: 1 1 auto; } /* starts at its natural content size, then grows/shrinks from there */flex-basis of 0%, the item's own content size is entirely ignored as a starting point — every item with flex: 1 ends up genuinely equal-width, regardless of how much text each one contains. With flex: 1 1 auto, each item starts from its own natural content width and then grows from there — items with more content can end up visibly wider than items with less, even though all of them share the same flex-grow: 1. Reach for flex: 1 specifically when items must be perfectly equal regardless of content.A Pricing Table Bug at a Chicago SaaS Company
The marketing team ships a three-tier pricing table — Starter, Pro, Enterprise — as a horizontal row of cards. The "Pro" plan name is short. The "Enterprise" plan card, added a week later, needs an extra line of description text explaining custom SSO and dedicated support. As soon as it ships, the cards visibly stop matching widths: the Enterprise card is noticeably narrower than the other two, even though all three share the same CSS class.
.pricing-row {
display: flex;
gap: 24px;
}
.pricing-card {
flex: 1 1 auto; /* the culprit */
padding: 32px;
border-radius: 12px;
background: white;
}What the engineer finds
Exactly the distinction from Part 08's callout: flex: 1 1 auto starts each card's width from its own natural content size before growing — and the Enterprise card's extra sentence of description text gives it a larger natural content width than Starter or Pro. All three cards share the same flex-grow: 1, so they grow by equal amounts of extra space from there, but that equal amount is added on top of different starting sizes, so the final widths never converge to equal.
.pricing-card {
flex: 1; /* shorthand for flex: 1 1 0% — ignores each card's own content
width as a starting point entirely, so all three grow from
an identical zero baseline and end up genuinely equal-width */
padding: 32px;
border-radius: 12px;
background: white;
}The cards become pixel-identical in width regardless of how much copy any individual plan needs, and the fix survives the inevitable next update where a fourth "Team" tier gets added with yet another different amount of text. This exact flex: 1 vs flex: 1 1 auto distinction is one of the most common real Flexbox debugging sessions — "these should be equal width and they are not, despite identical CSS classes."
Five Misconceptions About Flexbox
6 Interview Questions — With Complete Answers
Flexbox Mistakes Beginners Make Constantly
Rendering Bugs You Will Hit — And Exactly Why
🎯 Key Takeaways
- ✓display: flex creates a main axis and a cross axis. flex-direction chooses which physical direction is the main axis — every other Flexbox property is defined relative to these two axes, not to literal horizontal/vertical.
- ✓justify-content distributes space along the MAIN axis; align-items aligns items across the CROSS axis within one line; align-content distributes multiple WRAPPED LINES across the cross axis and does nothing with a single line.
- ✓flex-grow distributes leftover space as a RATIO relative to sibling flex-grow values, only after each item's base size is accounted for — it is not a percentage of the container.
- ✓flex-shrink (default 1) lets items shrink below their base size when the container is too tight; flex-shrink: 0 protects a specific item (like a sidebar or icon) from ever shrinking.
- ✓flex-basis sets an item's starting size before grow/shrink math runs — flex: 1 (basis 0%) ignores content size entirely for genuinely equal widths; flex: 1 1 auto starts from each item's own natural content size instead.
- ✓flex-wrap defaults to nowrap — items shrink to stay on one line rather than wrapping automatically. flex-wrap: wrap must be set explicitly for items to flow onto new lines.
- ✓row-reverse and column-reverse only flip the VISUAL order — DOM order and keyboard tab order stay unchanged, a real accessibility consideration for interactive content.
What comes next
Module 24 takes every property from this module and applies it to real UI patterns — a responsive navbar, an equal-height card grid, the single-container centering reflex, a sticky footer layout, and the gap property.
Module 24 → Flexbox in Practice — Real LayoutsDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.