Skip to content

6.2.4 — Flexbox & Grid

Three cards in a row, and the one with more text is wider than the other two:

css
.row  { display: flex; gap: 16px; }
.card { flex: 1 1 auto; }     

Change one character and they become exactly equal:

css
.card { flex: 1 1 0; }        

Nothing about the container changed. The difference is in a single value most people copy without reading, and understanding it is most of understanding flexbox.

1. Axes: the idea everything else hangs off

A flex container has a main axis and a cross axis. flex-direction decides which is which, and every other property is described in terms of those two words rather than in terms of left, right, top and bottom.

flex-direction: rowmain axis — justify-contentcross — align-itemsflex-direction: columnmain axiscross axis
Switch flex-direction and the two axes swap. Nothing else in your CSS has to change, which is the entire point of naming them main and cross.

justify-content always distributes along the main axis. align-items always aligns along the cross axis. In a row those mean horizontal and vertical; in a column they mean the reverse. This is the single fact that makes flexbox stop feeling arbitrary, and the reason so many people guess is that they memorised "justify is horizontal", which is only true half the time.

2. The container's six properties

css
.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;      /* along the main axis */
  align-items: stretch;             /* along the cross axis, per line */
  align-content: stretch;           /* the LINES themselves — only when wrapped */
  gap: 0;                           /* space between items */
}

flex-wrap: nowrap is the default, and it means items shrink rather than wrap. A row of eight cards in a narrow container squeezes all eight into the width instead of moving some to a second line. That is a deliberate default — flexbox was designed for user-interface toolbars, where wrapping is usually wrong — but it surprises people coming from a print mindset.

align-items: stretch is also the default, and it explains a behaviour people often attribute to something else: flex children are the same height as the tallest one, automatically, with no equal-height hack. Set align-items: flex-start if you do not want that.

align-content only does anything when there are multiple lines, meaning flex-wrap: wrap is on and the items actually wrapped. On a single line it is inert, which is why it appears not to work — nothing is wrong, there is simply only one line to distribute.

gap replaced the margin tricks completely. Before it, spacing between flex items meant margin-right on every item plus a :last-child rule to remove the trailing one, or negative margins on the container. gap applies only between items, works identically in flex and grid, and is one of the genuinely uncomplicated improvements of the last decade.

Centring, for the record, since it was hard for fifteen years and is now one line:

css
.centre { display: flex; justify-content: center; align-items: center; }

3. flex-grow, flex-shrink, flex-basis — and the shorthand

Now the opening example.

flex-basis is the item's starting size along the main axis, before any growing or shrinking. It behaves like width in a row and like height in a column, and it beats width when both are set. flex-basis: auto means "use my width, or if I have none, use my content's size".

flex-grow is a share of the leftover space. It is a ratio, not a size: with three items at grow 1, 1 and 2, the leftover is split into four parts and the third item takes two of them.

flex-shrink is a share of the overflow to remove, and it is weighted by basis — a detail almost nobody knows, covered below.

The shorthand hides all of this, and the four common forms are worth memorising because they are what you will actually type:

ShorthandExpands toBehaviour
flex: 11 1 0%Equal shares; content size ignored
flex: auto1 1 autoGrows, but starts from content size
flex: none0 0 autoRigid at content size
flex: initial0 1 autoThe default: shrink but never grow

Now the opening example resolves. With flex: 1 1 auto each card starts at its content width, and grow distributes only the leftover space equally on top of those different starting points — so the card with more text stays wider. With flex: 1 1 0 every card starts at zero, so the entire width is leftover space, split equally. Equal columns need a basis of zero, not a grow of one.

flex: 1 is the one you want for equal columns, and it is worth knowing that flex: 1 and flex: 1 1 auto are different, because they read as if they should not be.

The shrink formula, and why the wide item shrinks more

When items overflow, shrinkage is weighted by each item's basis:

shrink amount ∝ flex-shrink × flex-basis

A 600-pixel item and a 200-pixel item, both with flex-shrink: 1, in a container that is 200 pixels short: the weights are 600 and 200, total 800, so the large item gives up 150 pixels and the small one gives up 50. They shrink in proportion rather than equally.

This is the correct behaviour — proportional shrinking preserves the visual relationship between the items — but it explains why a small sidebar next to a large main area does not squash the way you expected. If you want the sidebar rigid, say so with flex-shrink: 0.

The trap: min-width: auto

This one costs people hours, and once you know it you will spot it instantly.

html
<div class="row">
  <div class="item">A very long single line of text that should be truncated…</div>
</div>
css
.row  { display: flex; }
.item { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

The text does not truncate. It overflows the container and pushes the layout wide, ignoring overflow: hidden completely.

The cause: a flex item's min-width defaults to auto, not to 0. That means an item refuses to shrink below its own minimum content size — for unwrappable text, the width of the whole line. The specification chose this default to stop content vanishing, which is a reasonable default and an infuriating one here.

css
.item { flex: 1; min-width: 0; }   

That is the fix, and it is the same fix in a column with min-height: 0. Any time a flex child overflows instead of shrinking — a long word, a table, a <pre> block, a nested flex container, a canvas — reach for min-width: 0 first. overflow: hidden on the item also works, because setting overflow to anything other than visible changes the automatic minimum to zero, which is why that fix appears to work by accident.

4. Grid: two dimensions at once

Flexbox lays out along one axis and lets the other fall where it may. Grid defines both up front.

css
.layout {
  display: grid;
  grid-template-columns: 240px 1fr 300px;   /* (1) three explicit columns */
  grid-template-rows: auto 1fr auto;        /* (2) three explicit rows */
  gap: 24px;
  min-height: 100svh;
}

Line (1) creates three columns: a fixed sidebar, a flexible middle, a fixed panel. Line (2) makes the header and footer take their content height while the middle row absorbs everything left over.

fr is the unit that makes grid work. It means a fraction of the space remaining after fixed tracks and gaps are subtracted. 1fr 1fr is two equal columns; 2fr 1fr gives the first twice the second. Unlike a percentage, it accounts for gap automatically, which is why repeat(3, 1fr) with a gap does exactly what you meant and width: 33.33% does not.

One fr caveat with the same shape as the flexbox one: 1fr is shorthand for minmax(auto, 1fr), so a track will not shrink below its content. A grid column containing a wide table overflows for exactly the reason the flex item did. minmax(0, 1fr) is the fix, and it is the grid spelling of min-width: 0.

Repeating, and the responsive grid with no media query

css
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}

Read it right to left. minmax(220px, 1fr) says each column is at least 220 pixels and otherwise shares the space equally. repeat(auto-fill, …) says fit as many such columns as will go. The result is a gallery that goes from one column on a phone to six on a wide monitor, reflowing continuously, with no breakpoints at all. This single line replaced an enormous amount of media-query code and is the strongest argument for grid on its own.

auto-fill versus auto-fit is the follow-up, and the difference only shows with few items. auto-fill keeps the empty tracks, so three cards in a container wide enough for six sit in the left half. auto-fit collapses the empty tracks, so those three cards stretch to fill the row. Neither is right in general — auto-fit when items should always fill the width, auto-fill when a consistent card size matters more than filling the row.

Named areas, which are the readable way to do page layout

css
.page {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "sidebar header"
    "sidebar main"
    "sidebar footer";
}
.page > header  { grid-area: header; }
.page > .side   { grid-area: sidebar; }
.page > main    { grid-area: main; }
.page > footer  { grid-area: footer; }

The ASCII picture in grid-template-areas is the layout, and a repeated name spans cells. Rearranging the whole page for a different screen size means rewriting three lines inside a media query and nothing else:

css
@media (max-width: 720px) {
  .page {
    grid-template-columns: 1fr;
    grid-template-areas: "header" "main" "sidebar" "footer";
  }
}

Note what just happened: the sidebar moved from the left of the page to below the main content, and the HTML did not change. That is real power and it carries a real warning — the visual order and the DOM order are now different, and keyboard focus follows the DOM, not the grid. A user tabbing through the mobile layout would jump into the sidebar before the main content if you had reordered the other way. Reorder deliberately, and check the tab order afterwards. The same warning applies to flexbox's order and row-reverse, which is why order should be used sparingly.

Placing an item by line number

Grid lines are numbered from 1, and negative numbers count from the end, which is how you span a full row without knowing how many columns there are:

css
.hero { grid-column: 1 / -1; }        /* first line to last line */
.wide { grid-column: span 2; }        /* two tracks, wherever it lands */
.exact{ grid-column: 2 / 4; grid-row: 1 / 3; }

grid-column: 1 / -1 is worth remembering as an idiom; it is the grid equivalent of "full width".

The implicit grid

Place more items than your explicit tracks hold and grid creates rows automatically. Their size comes from grid-auto-rows, and the direction from grid-auto-flow:

css
.feed {
  grid-auto-rows: minmax(120px, auto);   /* implicit rows: at least 120, grow if needed */
  grid-auto-flow: row dense;             /* backfill holes left by spanning items */
}

dense fills gaps left when a wide item could not fit, which looks tidier — and, again, moves items away from source order visually while leaving the tab order alone. Use it for a photo mosaic, not for anything interactive.

subgrid

The long-standing weakness of grid was that a child grid could not align to its parent's tracks. Cards in a row each containing a title, body and button could not line their buttons up unless every title happened to be the same height.

css
.card {
  grid-row: span 3;              /* the card occupies three parent rows */
  display: grid;
  grid-template-rows: subgrid;   /* use the parent's rows */
}

Now every card's title, body and footer share the parent's row tracks, so all the titles are the same height and all the buttons line up, no matter how much text each card holds. This is one of the few CSS features that removed a whole category of JavaScript workaround.

5. Choosing between them

The rule that holds up:

Grid when you are placing things into a layout you have designed. Two dimensions, or one dimension where the track sizes are the point. Page shells, dashboards, galleries, forms with aligned labels, anything you could sketch on graph paper.

Flex when you are distributing things along a line and the content decides the sizes. Toolbars, button groups, a label next to a value, a navigation bar, a card's footer with a price on the left and a button on the right.

Two more distinctions that decide real cases:

Wrapping. Flex items wrap as a stream and each line sizes itself independently, so wrapped rows have ragged track widths. Grid tracks are defined once and every row shares them. If the wrapped rows must stay aligned in columns, that is grid.

Gaps and overlap. Grid can leave a cell empty or deliberately overlap two items in the same cell — a caption over an image, without absolute positioning. Flex has no concept of a cell to leave empty.

They compose. A grid page shell whose header is a flex row is the normal arrangement, not a compromise.

One note on writing direction

Everything on this page has a logical spelling: inline-start and block-end instead of left and top, margin-inline instead of margin-left/margin-right, padding-block for the vertical pair. In a right-to-left language, left stays left and inline-start becomes the right edge.

Flexbox and grid already work this way — flex-start in a right-to-left document is on the right — which is why a flex layout usually mirrors correctly for Arabic or Hebrew with no work while a float-based one does not. Using margin-inline-start rather than margin-left extends the same property to your own spacing, and it costs nothing to do from the start.

What the interviewer will push on

"What does flex: 1 expand to, and why does it matter?" 1 1 0%. It matters because flex: 1 1 auto starts from content size, so items end up unequal. Equal columns need a zero basis. This is the flexbox question most often asked and most often answered with "it makes them grow", which does not distinguish the two.

"A flex item with text-overflow: ellipsis is not truncating. Why?" A flex item's min-width defaults to auto, so it will not shrink below its content's minimum size. min-width: 0. If you can also say that overflow: hidden on the item happens to fix it — by changing the automatic minimum — you have understood the mechanism rather than memorised the cure.

"When would you choose grid over flexbox?" Two dimensions, or one dimension where track sizes must be consistent across wrapped rows. Flex when content sizes drive the layout. Then give a concrete pair: page shell in grid, its header's contents in flex.

"How do you build a responsive card gallery with no media queries?" repeat(auto-fill, minmax(220px, 1fr)). The follow-up is auto-fill versus auto-fit, and the answer is about what happens when there are too few items to fill a row.

"Why does my grid column overflow when it contains a table?" 1fr means minmax(auto, 1fr) and the auto minimum is the content's minimum size. Use minmax(0, 1fr). Pointing out that this is the same underlying rule as flexbox's min-width: auto is the answer that shows depth.

"What are the risks of reordering with grid areas or order?" Keyboard focus and screen-reader order follow the DOM, not the visual layout. A visually reordered page can become unusable by keyboard. Always check the tab order after reordering.

One thing to volunteer: mention subgrid, and specifically the problem it solves — aligning content across sibling cards without measuring anything in JavaScript. It is a recent feature, it removes a real hack, and knowing what it is for rather than that it exists is the distinguishing part.

Recall

  • Flexbox thinks in main axis and cross axis, which swap with flex-direction. justify-content is always main, align-items is always cross — that is why "justify is horizontal" is only half right.
  • Defaults that surprise: flex-wrap: nowrap (items squeeze rather than wrap), align-items: stretch (equal heights for free), and align-content doing nothing unless there are multiple lines.
  • flex: 1 = 1 1 0% (equal columns) · flex: auto = 1 1 auto (grows from content size) · flex: none = 0 0 auto · default 0 1 auto. Equal columns need a zero basis.
  • Shrinking is weighted by basis, so a wide item gives up proportionally more than a narrow one.
  • A flex item's min-width defaults to auto, so long text overflows instead of truncating. min-width: 0 (or min-height: 0 in a column). Grid's version of the same rule: 1fr is minmax(auto, 1fr), so use minmax(0, 1fr).
  • fr is a share of space after fixed tracks and gaps, which is why it beats percentages.
  • repeat(auto-fill, minmax(220px, 1fr)) gives a fully responsive gallery with no media queries. auto-fit collapses empty tracks so few items stretch; auto-fill keeps them so card size stays consistent.
  • grid-template-areas makes the layout readable and re-arrangeable in three lines — but focus order follows the DOM, not the grid, so reordering visually can break keyboard use. Same warning for order and dense.
  • grid-column: 1 / -1 spans full width. subgrid lets child grids share the parent's tracks, which aligns content across sibling cards without JavaScript.
  • Grid for designed two-dimensional layout, flex for content-driven distribution along a line. They compose.

Self-test: Why are flex: 1 and flex: 1 1 auto different on screen? · Why does text-overflow: ellipsis fail inside a flex row? · What is the grid equivalent of that same bug? · What does auto-fit do that auto-fill does not? · What accessibility check must follow any visual reordering?

Next: 6.2.5 covers the third dimension and the responsive one — why z-index: 9999 sometimes loses to z-index: 1, what a containing block really is, and how to write layouts that respond to their container rather than to the window.