Lesson 12 of 25

CSS Flexbox

A Layout Tool for One Direction at a Time

Flexbox arranges a set of items along a single line — a row or a column — and gives you precise control over how the leftover space is shared out. That single-direction focus is its defining feature. If you need to control rows and columns together, Grid is the right tool, and a later lesson covers it.

You switch it on with one declaration on the parent: display: flex. From that moment the parent is a flex container and its direct children become flex items. Grandchildren are unaffected — flexbox only reaches one level down. Notice what changes immediately: the children line up horizontally, and block-level children stop taking a full line each because they are no longer in normal flow.

Two ideas carry the rest of the lesson. The main axis is the direction items are laid out along, set by flex-direction. The cross axis is perpendicular to it. Every alignment property in flexbox acts on one axis or the other, and once you know which is which, the property names stop being arbitrary.

Example
/* One declaration turns the children into flex items */
.toolbar {
  display: flex;
}

/* The four directions */
.row          { flex-direction: row; }             /* default: left to right */
.row-reverse  { flex-direction: row-reverse; }     /* right to left */
.column       { flex-direction: column; }          /* top to bottom */
.column-rev   { flex-direction: column-reverse; }  /* bottom to top */

/* Space between items — never around them */
.toolbar { gap: 16px; }
  • display: flex goes on the parent, not on the items
  • Only direct children become flex items
  • Main axis — the direction items flow, controlled by flex-direction
  • Cross axis — always perpendicular to the main axis
  • Defaults: direction row, no wrapping, items stretched to equal height
  • gap spaces items apart without any margin arithmetic

justify-content and align-items: Which Axis Each One Uses

This is the single most misremembered pair in CSS, and the confusion comes from learning them as "horizontal" and "vertical". They are not. justify-content always works along the main axis, and align-items always works along the cross axis.

With the default flex-direction: row, the main axis runs left to right, so justify-content feels horizontal and align-items feels vertical. Change to flex-direction: column and the two swap completely: now justify-content positions items vertically and align-items positions them horizontally. Anyone who memorised the horizontal/vertical shortcut breaks at exactly this moment — usually when making a row stack into a column on mobile — and cannot work out why their centring inverted.

The values reflect this. justify-content distributes free space along the line: flex-start, center, flex-end, space-between (first and last items pinned to the edges, gaps equal in between), space-around and space-evenly. align-items positions items across the line: stretch (the default, which is why flex cards end up equal height for free), flex-start, center, flex-end and baseline, which lines up the text baselines of items with different font sizes.

That default stretch is worth pausing on, because it silently solves a problem that used to be genuinely hard. Three cards with different amounts of text inside a flex row automatically end up the same height. If you set align-items: center you lose that, and the cards become ragged — so only change it when you mean to.

Example
/* Row: main axis is horizontal */
.navbar {
  display: flex;
  justify-content: space-between;   /* logo left, links right */
  align-items: center;              /* vertically centred */
}

/* Column: the SAME two properties now act the other way round */
.hero {
  display: flex;
  flex-direction: column;
  justify-content: center;   /* now this is the VERTICAL one */
  align-items: center;       /* now this is the HORIZONTAL one */
  min-height: 70vh;
}

/* Perfect centring in both directions, whatever the direction */
.centre {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* Override the cross-axis alignment for one item only */
.toolbar .logo { align-self: flex-start; }
Notes
  • align-content is a third property that only does something when items are wrapping onto multiple lines — it distributes the lines along the cross axis. On a single-line flex container it has no effect at all, which is why it so often appears to be broken.

Sizing Items: grow, shrink and basis

Three properties decide how much space each item gets, and the flex shorthand sets all three at once. flex-grow is how greedily an item claims spare space, as a share. flex-shrink is how readily it gives space up when there is not enough. flex-basis is the size it starts from before any growing or shrinking happens.

Two shorthand values cover most situations, and the difference between them matters. flex: 1 expands to 1 1 0% — the basis is zero, so every item starts from nothing and the whole width is divided by the grow factors. The result is genuinely equal widths regardless of how much text each item contains. flex: auto expands to 1 1 auto — each item starts at its natural content width and only the leftover space is shared, so an item with more text stays wider. When your "equal" columns come out unequal, this is almost always the reason.

Different grow values give proportions rather than equality. Two items at flex: 1 and one at flex: 2 divide the row into quarters: one, one and two. And flex: 0 0 240px is the way to say "this sidebar is exactly 240 pixels and must not grow or shrink" while a neighbouring flex: 1 main area absorbs everything else.

Example
/* Equal columns, whatever the content length */
.cols > * { flex: 1; }          /* = flex: 1 1 0% */

/* Content-sized columns that share only the spare space */
.cols > * { flex: auto; }       /* = flex: 1 1 auto */

/* Proportions: 1 : 1 : 2 */
.a { flex: 1; }
.b { flex: 1; }
.c { flex: 2; }

/* A fixed sidebar next to a flexible main area */
.layout        { display: flex; gap: 24px; }
.layout .side  { flex: 0 0 240px; }   /* never grows, never shrinks */
.layout .main  { flex: 1; }           /* takes everything else */

/* Push one item to the far end of the row */
.toolbar .logout { margin-left: auto; }
  • flex-grow — share of the spare space this item claims (0 means do not grow)
  • flex-shrink — how willingly it shrinks when space runs short (0 means never)
  • flex-basis — the starting size before growing or shrinking
  • flex: 1 = 1 1 0% — truly equal widths
  • flex: auto = 1 1 auto — content-based widths that share the leftovers
  • margin-left: auto on one item pushes it and everything after it to the far end

Wrapping, and the Two Bugs Everybody Hits

By default a flex container never wraps — it squeezes items narrower and narrower rather than moving any onto a second line. On a phone that produces columns three characters wide. flex-wrap: wrap allows a new line, and combined with a flexible basis it gives you a responsive layout with no media query at all: flex: 1 1 250px means "aim for 250 pixels, grow to fill the row, and wrap when you cannot have 250". Three cards become two, then one, as the screen narrows.

The first classic bug is text refusing to shrink. A flex item will not shrink below the size of its content, because items have an automatic minimum size in the main axis direction. So a long unbroken word or a line you meant to truncate with an ellipsis pushes its item wider and blows out the layout instead. The fix is min-width: 0 on that item — it lifts the automatic minimum and lets the item shrink so overflow: hidden and text-overflow: ellipsis can finally do their job. In a column layout the same fix is min-height: 0.

The second is trying to control flex behaviour from the wrong place. display: flex, justify-content, align-items, flex-wrap and gap all belong on the container. flex, align-self and order belong on the items. Writing justify-content on a child does nothing whatsoever, and it is a five-minute mistake that everybody makes at least once.

Example
/* Responsive cards with no media query */
.cards {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
}
.cards .card {
  flex: 1 1 250px;   /* target 250px, grow to fill, wrap when it cannot */
}

/* Bug 1: the text will not truncate */
.row      { display: flex; gap: 12px; }
.row .name {
  min-width: 0;               /* <- without this, nothing below works */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Bug 2: property on the wrong element */
.card { justify-content: center; }   /* does nothing — .card is an item */
.cards { justify-content: center; }  /* correct — .cards is the container */
Flexbox: change the axis and watch alignment swap
HTML
<h3>Row — justify-content is horizontal</h3>
<div class="box row">
  <div class="item">1</div>
  <div class="item tall">2</div>
  <div class="item">3</div>
</div>

<h3>Column — the same two properties, swapped</h3>
<div class="box column">
  <div class="item">1</div>
  <div class="item tall">2</div>
  <div class="item">3</div>
</div>

<h3>Wrapping cards: flex: 1 1 220px</h3>
<div class="cards">
  <div class="card"><h4>Short</h4><p>A little text.</p></div>
  <div class="card"><h4>Longer</h4><p>This card has noticeably more text inside it than the others do.</p></div>
  <div class="card"><h4>Medium</h4><p>Some text here.</p></div>
</div>
CSS
body {
  font-family: system-ui, Arial, sans-serif;
  padding: 24px;
  background: #f4f4f7;
}

h3 { font-size: 15px; color: #555555; margin: 24px 0 8px; }

.box {
  display: flex;
  background: #ffffff;
  border: 1px dashed #b9b9c6;
  padding: 12px;
  gap: 12px;
  min-height: 180px;

  /* Change these two and compare the row with the column */
  justify-content: space-between;
  align-items: center;
}

.row    { flex-direction: row; }
.column { flex-direction: column; }

.item {
  background: #d1039e;
  color: #ffffff;
  padding: 16px 24px;
  border-radius: 8px;
}

.tall { padding-block: 32px; }

/* Wrapping cards — no media query needed */
.cards {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.card {
  flex: 1 1 220px;      /* try flex: auto and compare the widths */
  background: #ffffff;
  border: 1px solid #e3e3ea;
  border-radius: 10px;
  padding: 16px;
}

.card h4 { margin: 0 0 8px; color: #d1039e; }
.card p  { margin: 0; color: #555555; font-size: 14px; }
Notes
  • The order property can rearrange items visually, but it does not change the order they appear in the HTML — so keyboard focus still moves in source order. Using it to shuffle interactive elements produces a page where tabbing jumps around the screen unpredictably. Change the markup instead, or use flex-direction: column-reverse for the simple case.
Ask AI