Skip to content
Chapter 20Lesson 3

Flexbox, the 1D primitive

Flexbox, the CSS one-dimensional layout system you drive with Tailwind utilities to arrange elements in a single row or column.

Open any SaaS app and the first thing on the screen is the same shape: a top bar with a logo pinned to the left, a sign-in button pinned to the right, and everything sitting on one tidy horizontal line. In 2026 the entire bar is three utilities, flex items-center justify-between, and a web developer writes that string without thinking. By the end of this lesson you’ll read it and know exactly what each of the three does, and you’ll build four more layouts that lean on the same handful of ideas.

In the last lesson you turned flex on as a display mode. It changed an element’s inner formatting context so its children stopped stacking and started lining up in a row. This lesson is about the algorithm behind that switch: the one that takes a row or column of children with unpredictable content widths and decides who gets how much space, where the leftover slack goes, and how everything aligns. It rests on one idea that’s easy to skip past, that a flex container has two axes. Once the two axes are clear in your head, choosing between justify and items stops being a guess. One more utility, min-w-0, keeps your flex rows from blowing out their container. Those two ideas account for most of the gap between someone who has heard of flexbox and someone who ships it confidently.

Setting display: flex (Tailwind flex) on an element makes it a flex container . Every direct child automatically becomes a flex item : there’s no per-child opt-in and no class you add to the children. Turn the parent into a flex container and its children come along automatically.

A flex container lays its items out along two perpendicular directions, and the rest of the lesson builds on this fact. The main axis is the direction the items flow, which by default is a horizontal row running left to right in a left-to-right language. It follows the writing direction, so in a right-to-left language the same row runs right to left, a detail we’ll trust rather than chase here. The cross axis is the one perpendicular to it, vertical in a default row.

The habit to build, before you touch a single utility, is that every flex property targets one axis. Properties that distribute items along the flow work the main axis, and properties that position items across the flow work the cross axis. So the first question is never “which utility do I want” but “which axis am I moving things on,” and the utility follows from the answer. Name the axis first, then pick the property. Most flexbox mistakes a newcomer makes come from reaching for a property before naming the axis it belongs to.

Naming the axis first matters because the axes are not fixed to “horizontal” and “vertical.” The main axis is wherever the layout flows. Flip a row into a column and the main axis rotates to vertical, dragging the cross axis to horizontal with it. In the figure below, switch between the Row and Column tabs and watch the two arrows rotate together. The boxes barely move; what changes is which arrow is labelled “main.”

cross axis
1
2
3
main axis
Default row: the main axis runs horizontally, the cross axis perpendicular to it.

That is the whole foundation: two named axes, and a rule that every property belongs to one of them. The next four sections fill in which properties live on which axis, and because you’ll name the axis first, none of them will surprise you.

Which direction the main axis runs is yours to choose. flex-row is the default, so items flow left to right. flex-col rotates the main axis to vertical so items stack top to bottom, which you just saw in the column tab. Row or column is the choice you’ll make on essentially every flex container.

There are also flex-row-reverse and flex-col-reverse, worth knowing mainly so you know not to reach for them casually. They flip the visual order of the items but leave the source order untouched, meaning the order in the HTML, which is also the order the keyboard tabs through. So a flex-row-reverse bar looks like it runs right to left, but pressing Tab still walks the items left to right in the DOM. That mismatch is an accessibility bug, because a sighted keyboard user sees focus jump backwards across the screen. When you genuinely need to move one item, the better tool is order-* on that single item, which keeps the gap between visual and source order smaller. Treat the -reverse utilities as something to recognize, not something to reach for.

By default a flex container forces all its items onto a single line, shrinking them if it has to rather than letting any wrap to a new line. flex-wrap turns that off, so items that don’t fit flow onto the next line. You’ll meet wrapping properly when you build wrapping grids later in this chapter. For now, just know that flex-wrap exists and that once items wrap, gap-y-* becomes the gap between the rows. We’ll come back to spacing in a moment.

Here’s the question this section answers: when the container is wider than its items need, or narrower, who absorbs the difference? A nav bar is rarely the exact width of its contents. Something has to stretch to fill the slack, or give up space when there isn’t enough. Flexbox lets you decide, per item, exactly how that negotiation goes.

Three properties on each flex item drive it. flex-grow is how eagerly an item expands to eat free space. flex-shrink is how willingly it gives space back when there isn’t enough. flex-basis is the size each item starts from before any growing or shrinking happens. The flex property is shorthand for all three at once. It’s worth knowing these three exist, because every flex utility you write compiles down to them, but in real code you’ll almost never set them by hand. Instead you reach for three named shorthands that cover the cases that actually come up.

  • flex-1: grow and shrink, starting from a basis of zero. Because every flex-1 item starts from nothing and then grows equally, they end up sharing the available space in equal portions regardless of their content. Reach for it when something should fill the leftover room: a search input next to a button, or the main content column next to a sidebar.
  • flex-auto: grow and shrink too, but starting from the item’s content size. It hands out leftover space proportionally on top of what each item already needs, so a longer item stays wider than a shorter one. It is a subtle difference: equal columns, versus columns sized to their content and then stretched to fill.
  • flex-none: don’t grow, don’t shrink. The item stays exactly its content (or declared) width. Reach for it when an element’s size is part of the design and must not move.
  • shrink-0: keep the default growing behavior but switch off shrinking. This is the everyday reach for sidebars, fixed-width avatars, icons, and badges, anything that should hold its size while its neighbors flex around it.

That last one comes with a surprise worth flagging: flex items shrink by default. Put an avatar with a fixed w-12 into a tight row and flexbox will happily squash it narrower than 48px to make everything fit, distorting it. Adding shrink-0 tells flexbox to take that width literally. When an avatar or icon looks subtly crushed in a row, a missing shrink-0 is the usual culprit.

The fastest way to feel the difference between these is to move the slack yourself. In the playground below, drag the container width and watch the three boxes react. Then switch the middle box between flex-1, flex-auto, and flex-none, and notice how the leftover space gets handed out differently. With flex-none, the middle box ignores the negotiation entirely while its neighbors keep flexing.

The side boxes are always flex-1. Watch the middle box: flex-1 makes all three equal, flex-auto keeps the middle wider for its longer text, flex-none freezes it at its content width while the sides flex around it.

The playground hints at the flex-1 versus flex-auto difference, and this side-by-side pins it down. Both rows are three items wide, and the only change is the shorthand on each item. Read the basis difference in the prose under each tab.

<div className="flex gap-3">
<div className="flex-1">A</div>
<div className="flex-1">Medium label</div>
<div className="flex-1">Longer label here</div>
</div>

Equal columns. Every item starts from a zero basis, so all three end up the same width no matter how much text each holds; content length stops mattering.

You now have everything you need to size items. The shortcut to remember: flex-1 for “fill and share equally,” flex-none or shrink-0 for “don’t touch my size,” and flex-auto for the rarer “size to content, then stretch.”

Here the two-axis model starts to pay off. Alignment is the same move on each axis: pick the axis, then pick a value. Once you have internalized which axis is which, the utility names read themselves.

On the main axis, justify-* distributes the items along the flow. The values are justify-start (the default, packed at the start), justify-end, justify-center, justify-between (first and last item flush to the edges, the rest spread evenly between them), justify-around (equal space around each item), and justify-evenly (equal space between every item and the edges). justify-between is the nav-bar workhorse: it pushes the logo to one edge and the sign-in button to the other with no manual spacing.

On the cross axis, items-* positions the items across the flow. items-stretch is the default, and it is worth pausing on, because it is why a flex child fills the full height of a row even when its content is short. Unless you tell it otherwise, every flex item stretches to fill the cross axis. The other values pull items off that stretch: items-start, items-end, items-center, and items-baseline.

That last one, items-baseline, is a detail an experienced eye catches and a newcomer often misses. Picture a row with a large heading and a small timestamp beside it. items-center centers the two boxes, and because the boxes are different heights, the text ends up looking misaligned: the big text and the small text don’t sit on the same line. items-baseline aligns the text baselines instead, so the words sit on a common line the way they would in a sentence. Whenever you align text of different sizes in a row, baseline is usually what you actually want.

items-center
Revenue updated 2m ago
items-baseline
Revenue updated 2m ago
items-center aligns the boxes; items-baseline aligns the text. The dashed rose rule marks the big word's baseline — with items-center the small caption floats above it, with items-baseline the words share it. With mixed font sizes, baseline is usually what you want.

Two more utilities round out alignment, both recognition-level. self-* (align-self) overrides the cross-axis alignment for a single item: set the row to items-center but pin one avatar to the top with self-start. And content-* (align-content) positions the lines of a wrapped container relative to each other. You’ll rarely reach for it, because gap-y-* usually handles the spacing between wrapped rows on its own. Know the names exist, but don’t go looking for them.

The playground below lets you sweep through every combination. It drives a row of boxes with two dropdowns, one for justify-content and one for align-items. The container is deliberately tall so the cross-axis movement is obvious. Change the justify value and watch the boxes slide horizontally; change items and watch them move vertically. After a dozen sweeps the mapping becomes muscle memory: in a default row, justify is the roughly horizontal one (main) and items is the roughly vertical one (cross).

Sweep justify-content and watch the boxes slide left-right along the main axis. Sweep align-items and watch them move top-bottom along the cross axis. With align-items: stretch the boxes fill the full height; every other value lets them fall back to their natural height, so the cross-axis position becomes visible.

Before moving on, practice the classification the whole section turns on. Drag each utility into the axis it controls. The tricky ones are the chips that sound like they belong to one axis but belong to the other: self-end, for instance, is a cross-axis tool even though it doesn’t start with items.

Sort each flex utility into the axis it controls. Drag each item into the bucket it belongs to, then press Check.

Main axis Distributes items along the flow (justify-*)
Cross axis Positions items across the flow (items-* / self-*)
justify-between
justify-center
justify-evenly
justify-start
items-center
items-baseline
items-stretch
self-end
self-start

You’ve seen gap in every figure so far without comment, and that’s intentional: in this course gap is simply how you space things inside a flex or grid container. Every flex container with more than one item gets a gap-*.

What makes gap the right tool is exactly what it does: it adds space between items and nowhere else. There’s no leading gap before the first item or trailing gap after the last, so it never collides with the container’s own padding. The items sit snug against the padding edge, with even gaps only in the interior. gap-x-* and gap-y-* set the two axes independently when you need asymmetry, and as noted earlier, gap-y-* is the gap between rows once items wrap.

You met the rule that sibling margins are forbidden back when we covered the spacing scale, and gap is the reason that rule costs you nothing: it does the spacing job those margins used to do. The full comparison, why gap beats the old margin and space-x tricks, gets its own treatment later in this chapter. For now, assume gap and use it.

This is the single most common flexbox bug you’ll hit in real work, and one worth committing to memory. It tends to show up in your first week on a real job, it’s confusing the first time you see it, and the fix is one utility.

The setup is ordinary. You build a row with flex: an avatar on the left, a flexible middle region with flex-1 holding some text (a file name, an email subject, a URL), and a button on the right. It works fine until the text gets long. Then, instead of the text truncating with an ellipsis the way you’d expect, the whole row grows wider than its container. The middle region refuses to shrink, so the button gets shoved off the edge or the row spills out of the card. flex-1 was supposed to make the middle flexible, so why won’t it shrink?

The reason isn’t obvious. Flex items default to min-width: auto, a rule that says never shrink an item below its min-content width. Min-content is, roughly, the width of the longest unbreakable thing inside: the longest word, or a long URL with no spaces to break on. So even though flex-1 says shrink freely, that min-width: auto floor quietly overrides it and clamps the item at the width of its longest word. The item can’t go narrower, so it pushes everything else out instead. Images and form controls carry their own implicit min-content widths, so the same blowout happens with an <img> or an <input> in a flex row.

The fix is to lower that floor. Add min-w-0 to the flexing item and you override the min-width: auto, which lets the item shrink below its content width, at which point the text finally truncates or wraps as you intended. Pair it with truncate (a text utility that clips the overflow to a single line with an ellipsis) for the classic one-line-with-ellipsis treatment. So the real, complete recipe for a flexible text region in a row isn’t flex-1 on its own, it’s flex-1 min-w-0, plus truncate when you want the ellipsis. flex-1 alone is the trap.

The before-and-after is the clearest way to see it. The first tab is the broken row, with flex-1 only and the long string spilling. The second adds min-w-0 truncate and the row behaves.

<div className="flex items-center gap-3">
<div className="size-10 shrink-0 rounded-full bg-violet-200" />
<p className="flex-1">
Re: invoice https://ledger.app/billing/invoices/2026-Q3-reconciliation-forecast
</p>
<button className="shrink-0 rounded-md bg-blue-600 px-3 py-1 text-white">Open</button>
</div>

The row blows out. flex-1 says shrink freely, but the item’s default min-width: auto floor stops it at its longest unbreakable run, here the URL, so the text pushes the button off the edge instead of truncating.

Now reproduce the bug and fix it yourself. The row below overflows, and your job is to make the middle text truncate so the layout holds, matching the target. The fix is the one class you just learned, plus truncate for the ellipsis.

This row overflows its card — the long subject pushes the Open button off the right edge. Add the one utility you just learned (plus `truncate` for the ellipsis) to the middle `<p>` so the subject text shrinks and truncates, and match the target.

Target
Your output LIVE

Everything so far comes together here. The goal of this lesson isn’t that you can recite flex properties; it’s that you compose layouts the way an experienced developer does, from a small, settled set of patterns rather than reinventing the CSS each time. These five patterns cover the overwhelming majority of one-dimensional layout you’ll write in a SaaS app, so learn them as whole units. Each tab below pairs a live rendering with its class string, so you can see the pattern and the code together and inspect either in devtools.

Ledger
<header className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<Logo />
<span className="font-extrabold">Ledger</span>
</div>
<nav className="flex items-center gap-4">
<a href="/pricing">Pricing</a>
<a href="/docs">Docs</a>
<button>Sign in</button>
</nav>
</header>
Logo on one side, actions on the other, everything centered. This is the app's top bar, and justify-between does the pushing.

A couple of those layouts lean on a sizing utility: h-full on the pinned-footer card, and widths like max-w-* that you’d want on a real nav bar. Those belong to sizing, which is its own topic later in this chapter; here they’re just along for the ride so the patterns render. The toolbar shows a quiet but powerful idea worth naming, which is using leftover space as a layout tool. An empty flex-1 div between two clusters grows to fill the slack and pushes them apart, with no manual margins at all. The pinned footer is the same trick rotated to the column axis: flex-col plus a flex-1 body region pushes the footer to the bottom regardless of how much the body holds. Once you see leftover space as something you can place and grow on purpose, a lot of “how do I push this down, or over there” questions answer themselves.

This brings the lesson back to where it started. You have the markup for a nav bar (a logo, some links, a sign-in button) sitting unstyled. Style it to match the target: logo pinned left, actions pinned right, everything on one centered line. This is the flex items-center justify-between string from the very first paragraph, and you now know exactly what each utility does.

Build the top nav bar from the intro. The logo sits on the left, the links and sign-in button on the right, everything on one centered line. Match the target with `flex`, an alignment utility, a distribution utility, and `gap` on the right-hand cluster.

Target
Your output LIVE

You’ve now built all five patterns, along with the nav bar the lesson opened on, on your own. The next lesson covers grid, the two-dimensional primitive for when one axis isn’t enough.

When a flex layout looks wrong, the better move is to look at what the browser is actually doing rather than guess at utilities. Open the Elements panel in Chrome or Firefox and you’ll see a small flex badge next to any flex container. Click it and the browser draws a colored overlay right on the page: the main and cross axes, the gaps between items, and the bounds of each item.

This is the two-axis model drawn out in front of you. The overlay shows the two axes this whole lesson has been about, so when items sit somewhere you didn’t expect, you can read off which axis they’re aligned on instead of cycling through justify and items values hoping one sticks. If an item won’t shrink, the overlay shows it pinned at its content width. If everything is bunched at one edge, the overlay shows the main axis and where the free space went. Reach for it the moment a flex layout surprises you.

Flexbox is best learned by playing with it. These let you keep moving the parameters around until the axis-and-property model is automatic.