CSS flexbox, a guide built on measured layouts
Flexbox is two properties and one axis rule. Here is that model, measured in a real browser, plus the two traps that make flex items misbehave.

CSS flexbox lays out items in one direction and distributes the space around them. You turn a parent into a flex container with display: flex, and its direct children become flex items that you position with two properties:
.container {
display: flex;
justify-content: center; /* along the main axis */
align-items: center; /* across it */
}
That is the entire model. justify-content works along the direction things flow, align-items works across it.
The reason flexbox still confuses people after they have memorised those two properties is that "along" and "across" are not fixed to horizontal and vertical. They swap. Everything below was measured in a real browser with getBoundingClientRect(), and the numbers make the swap impossible to argue with.
The one rule: the axes move
By default, flex-direction is row, so the main axis runs left to right and the cross axis runs top to bottom. justify-content is horizontal, align-items is vertical.
Set flex-direction: column and the two swap jobs. justify-content becomes vertical. align-items becomes horizontal.
Here is the same justify-content: center on the same two items, in the same 600 by 200 box, differing only by direction. Positions are relative to the container:
| Direction | Item A | Item B | Item width |
|---|---|---|---|
row |
x = 274.25 | x = 300.58 | 26.33 (content) |
column |
y = 63 | y = 100 | 598 (full) |
In row mode the items pushed toward the horizontal centre. In column mode the exact same property pushed them toward the vertical centre, and they went full width, because the cross axis is now horizontal and the default cross-axis behaviour is to stretch.
If flexbox is centering the wrong way, check flex-direction before anything else. That is the bug, roughly nine times in ten.
align-items defaults to stretch, and that surprises people
Put one short item in a tall flex container and measure its height:
align-items |
Item height |
|---|---|
default (stretch) |
198 |
flex-start |
37 |
The item is 37 pixels of actual content, and by default it grows to 198 to fill the container's cross axis. This is why a card with a coloured background suddenly becomes a tall stripe when you add display: flex to its parent, and why "my divs are all the same height now" happens without you asking for it.
Usually stretch is what you want, which is why it is the default. When it is not, align-items: flex-start is the fix.
| Value | Effect on the cross axis |
|---|---|
stretch |
Fill the container (default) |
flex-start |
Sit at the start, keep natural size |
flex-end |
Sit at the end |
center |
Centre, keep natural size |
baseline |
Line up the text baselines |
align-self overrides it on one item. Measured in a 120px-tall container with align-items: flex-start on the parent:
top y = 1 height = 37
center y = 41.5 height = 37
bottom y = 82 height = 37
stretch y = 1 height = 118
Four items, four different cross-axis behaviours, one property each.
Distributing space along the main axis
justify-content has six values, and three of them look similar until you measure them. Two items in a 600px container:
justify-content |
Item A x | Item B x |
|---|---|---|
flex-start (default) |
1 | 27.33 |
center |
274.25 | 300.58 |
space-between |
1 | 573.09 (last item) |
space-around |
137.63 | 437.2 |
space-evenly |
183.16 | 391.66 |
The difference between the last two is the one worth learning. space-around gives every item an equal margin on both sides, so the gap between two items is double the gap at the edges. space-evenly makes every gap identical, edges included. Look at the numbers: with space-around, item A sits at 137.63; with space-evenly it sits at 183.16, further in, because the outer gap grew to match the inner one.
space-between is the one you will reach for most, because it is how a navigation bar with a logo on the left and links on the right works.
flex: 1 versus flex-grow: 1, and why they differ
This is the trap that makes flex items refuse to be equal width, and the two look interchangeable.
Two items in a 600px row, one with the text "A" and one with a long label. Same container, only the property on the children differs:
| Property on children | Item A width | Item B width |
|---|---|---|
flex-grow: 1 |
172.69 | 425.31 |
flex: 1 |
299 | 299 |
Same intent, completely different result. The cause is flex-basis, the size an item starts at before any growing happens:
computed flex-basis with `flex-grow: 1` -> auto
computed flex-basis with `flex: 1` -> 0%
flex-grow: 1 sets only the grow factor, so flex-basis stays auto, meaning "start from my content size". Both items start at their natural widths, then share the leftover space equally. The item with more text was already wider, and stays wider.
flex: 1 is shorthand for flex: 1 1 0%. It sets the basis to zero, so both items start from nothing and split the entire space equally. Content length stops mattering.
Want equal columns? Use flex: 1. Want items to keep their proportions and just absorb the extra space? Use flex-grow: 1. Reaching for the wrong one is why a nav item with a long label hogs the row.
The overflow trap: min-width: auto
The second trap produces a layout that overflows its container for no visible reason, and no amount of width: 100% fixes it.
A flex item's default min-width is auto, not 0. That means a flex item will refuse to shrink below the width of its content, even when you told it to be flexible. Measured with a long unbreakable string in a 300px container:
Child min-width |
Child width | Container scrollWidth | Container clientWidth |
|---|---|---|---|
default (auto) |
307.53 | 358 | 298 |
0 |
247.98 | 300 | 298 |
In the first row the child is wider than its own container and the content overflows by 60 pixels. In the second, one line fixes it:
.flex-item {
flex: 1;
min-width: 0; /* allow shrinking below content width */
}
This is the fix for text that will not truncate with text-overflow: ellipsis inside a flex row, for a chart that pushes its sidebar off screen, and for the mysterious horizontal scrollbar on mobile. min-width: 0 on the flexible child, and it goes away.
Worth knowing: setting overflow: hidden on the item has the same effect, because min-width: auto only applies when overflow is visible. That is why the ellipsis pattern often works by accident.
Shrinking, and how to stop it
Items shrink below their stated size by default. Two items with width: 200px in a 300px container:
| Item widths | |
|---|---|
| default | 149 and 149 |
flex-shrink: 0 |
200 and 200 (container overflows) |
Neither is 200 in the first row. flex-shrink defaults to 1, so width on a flex item is a suggestion rather than an instruction.
flex-shrink: 0 is how you protect something that must keep its size: an icon, an avatar, a fixed sidebar. It is the most common one-property fix in a flex layout.
gap, and why it replaced margins
.container {
display: flex;
gap: 20px;
}
Measured: item A ends at 27.33, item B starts at 47.33. Exactly 20 pixels, and no margin on the last item to clean up afterwards.
Before gap, spacing flex children meant margin-right on every item plus a :last-child rule to remove it. gap has been supported in every current browser for years. There is no reason to write the margin version.
Wrapping onto multiple lines
Flexbox is single-line by default. Items shrink rather than wrapping, which is why three 120px cards squeeze into a 300px container instead of moving down.
.container {
display: flex;
flex-wrap: wrap;
}
Measured with three 120px items in a 300px container: items 1 and 2 sit at y = 1, and item 3 drops to y = 100. Two fit, the third wraps.
The common responsive pattern combines wrap with a basis:
.card {
flex: 1 1 250px; /* grow, shrink, but prefer 250px */
}
Cards sit side by side while there is room and wrap when there is not, with no media query involved.
order changes the picture, not the page
.first-in-html { order: 3; }
.second-in-html { order: 1; }
.third-in-html { order: 2; }
Measured x positions: the first element in the HTML rendered at 243.41, last in the row. The second rendered at 1, first.
Use this carefully. order changes visual position only. Keyboard focus and screen readers still follow the HTML, so a visitor tabbing through gets a different sequence than the one they can see. That is a real accessibility bug, not a theoretical one. Reorder your HTML when you can, and save order for cases where the same markup genuinely needs two different layouts.
margin: auto inside a flex container
The trick worth stealing. An auto margin absorbs all the free space in that direction:
.login-button {
margin-left: auto; /* pushes this item, and only this item, to the right */
}
Measured in a 600px navigation row with three items: logo at x = 1, nav at x = 49.05, and the button pushed out to x = 547.39.
This is the cleanest way to build a header. No wrapper divs, no space-between fighting your third item, one property on the thing you want pushed.
Flexbox or grid?
Both are current, neither replaced the other.
Flexbox is one-dimensional. Content decides sizes, and you lay out along a single line: a row of buttons, a navigation bar, a card's internals, anything that should wrap naturally.
Grid is two-dimensional. You define rows and columns up front and place items into them: page layouts, image galleries, anything where alignment in both directions has to line up across rows.
The quick test: if you are describing your layout as "a row of" or "a column of", it is flexbox. If you are drawing a table on paper, it is grid. They nest happily, and most real pages use grid for the page skeleton and flexbox inside the pieces.
The five things that fix most flex bugs
- Centering the wrong way? Check
flex-direction. The axes swapped. - Items unexpectedly tall?
align-itemsdefaults tostretch. - Columns not equal? Use
flex: 1, notflex-grow: 1. - Something overflowing or refusing to truncate?
min-width: 0on the flexible child. - An icon getting squashed?
flex-shrink: 0.
Measure it yourself
Every number here came from getBoundingClientRect() rather than from looking at a screenshot, and that is a habit worth copying. When a layout does something you did not ask for, open DevTools, select the element, and read the computed values. flex-basis: auto where you expected 0% explains an entire afternoon.
CSS Layout works through flexbox and grid with the page rendering live beside the code, so changing flex-direction and watching justify-content change meaning takes about four seconds.
Related reading: How to Center a Div for the six centering methods and their measured results, and How Do Websites Work for where CSS fits in the sequence between request and painted page.
More from the blog

Flexbox vs Grid: how to choose in ten seconds
Flexbox vs grid: flex sizes items to their content, grid sizes the tracks. Measured, that gave 32px and 229px against three equal 193px columns.
Read more
CSS specificity: why your style is not applying
Specificity is three numbers compared left to right, not one score. Here are the contests that decide it, measured in a browser.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.