← Back to Posts

flex-wrap: balance, or How to Stop Your Last Row Looking Broken

The row that ruins everything

Youve written this layout a hundred times. Cards, tags, logos, filter chips. display: flex, flex-wrap: wrap, a gap, and flex: 1 or flex: auto on the children so they fill the width. Looks great. Then someone adds a seventh item.

Three on the first line. Three on the second. And one poor item alone on the third, stretched across the entire container because flex: auto told it to eat every pixel of leftover space. A tag pill the size of a billboard. A logo card four times wider than its siblings. Nothing broke. It just looks like something broke, which as far as anyone reviewing your PR is concerned is the same thing.

Thats the last-row problem, and its probably the single most common reason people give up on wrapping flexboxes and reach for something else. Ian Kilpatrick, the Chrome engineer who wrote the explainer for the fix, put it plainly: without balancing, “it is trivial to create flex-lines which appear unbalanced (lots of whitespace in a particular line). This often makes developers avoid wrapping flexboxes, instead allowing content to overflow.”

Chrome 150 landed on 30 June 2026 with a one-word fix:

.cards {
  display: flex;
  flex-wrap: balance;
  gap: 12px;
}

Thats it. Thats the feature. What the browser does behind that keyword, and the eight years it took to get there, is where this gets interesting.

What we did instead, and why all of it was bad

Quick tour of the workarounds first, because knowing what youre replacing tells you what youre actually getting.

The classic is the ghost element. Stick an invisible filler child at the end with flex: auto and no height, so the last real row has something to share space with:

.cards::after {
  content: "";
  flex: auto;
}

Works, sort of. It stops the last item from stretching. It doesnt balance anything, though. You still end up with one item alone on the last line, just left-aligned now instead of stretched. Youve also stuck a pseudo-element in the box tree that some screen readers will happily announce if you get sloppy with content.

Then theres quantity queries: :nth-last-child(n+7) selectors that switch the layout based on child count. Fine when the count is known and small. Useless the moment the list is dynamic, and they cant see container width at all, which is the actual problem. How many items fit per line depends on available space, and no CSS selector has ever been able to measure that.

column-count does balance properly. Thats why people reached for it. But it fills top-to-bottom within each column, so the reading order goes all the way down column one before jumping to column two. For a card grid thats just wrong, and its wrong in a way that hurts keyboard and screen reader users rather than merely looking odd.

Grid with auto-fill is the one I still recommend most often. repeat(auto-fill, minmax(200px, 1fr)) gives you tidy equal columns, no stretched orphan, and it has worked everywhere for years. Keep using it. But grid forces every item into the same track width, so if your items have genuinely different intrinsic widths (tags, chips, buttons with labels of wildly varying length) grid flattens them all to the same size. Thats a different design, not a fix.

And then JavaScript. Measure everything, compute the breaks, apply widths. Perfect results, at the cost of a resize observer, layout thrash on every reflow, and a flash of unbalanced content before the script runs. The same deal we made with Masonry.js for about a decade.

All of these give something up. flex-wrap: balance doesnt, because the layout engine already knows every items size at the exact moment its deciding where to break.

The syntax

The formal grammar in CSS Flexible Box Layout Module Level 2 is:

flex-wrap = nowrap | [wrap | wrap-reverse] || balance

That || is doing real work. balance isnt a fourth sibling of nowrap, wrap and wrap-reverse; its a modifier that combines with them. So all of these are valid:

flex-wrap: balance;              /* balancing, wrapping implied */
flex-wrap: wrap balance;         /* the same thing, spelled out */
flex-wrap: wrap-reverse balance; /* balanced, lines stacked in reverse */

nowrap balance isnt, which makes sense. Nothing to balance if you never wrap.

The specs own description, tucked into the module interactions section, is that flex-wrap: balance “allows for flex items to be balanced across lines, so each line is as similar in length as possible.” Note length. Not item count. Ill come back to this later because it catches people out, but the short version is that balance is not trying to put four items on each row. Its trying to make each rows total content length come out roughly equal. For uniform items those happen to be the same thing. For mixed widths they arent, and length is the one that looks right to a human eye.

Spec editors, for the record: Tab Atkins Jr. at Google, Elika J. Etemad / fantasai at Apple, and Rossen Atanassov at Microsoft.

The algorithm is from 1981

This is my favorite part. The browser isnt doing anything new and clever to balance your cards. Its running a paper from 1981.

The Knuth-Plass line-breaking algorithm, from Donald Knuth and Michael Plass, is how TeX decides where to break a paragraph into lines. Its insight is that greedy breaking (cram as much as fits, move on, repeat) is locally optimal and globally awful. It produces precisely the failure mode were talking about: beautiful early lines, a wrecked final one. Knuth-Plass assigns a “badness” cost to every candidate set of breaks across the whole paragraph and picks the arrangement with the lowest total. It looks ahead instead of being greedy.

A wrapping flexbox is that same problem wearing different clothes. Flex items are words, flex lines are text lines, and each items hypothetical main size is its width. So Kilpatricks proposal is: run Knuth-Plass over the flex items hypothetical main sizes. Same math, different content model.

Two details from the explainer tell you what this costs you. First, it targets O(N log N), deliberately staying in the same complexity class as things flexbox already does, like sorting items by order. Not free, but not a quadratic trap either. Second, theres a fast path for uniform items. When everything is the same size, which is the common case for a card grid, the expensive search gets skipped entirely because the optimal answer is just arithmetic. You only pay for the real algorithm when your content actually varies.

Column-direction flexboxes need an extra step. In a row flexbox the container has a known width, so you know what youre breaking against. In a column flexbox there usually isnt a height constraint at all, so theres nothing to break against. The fix is bisection: search for the line-break size that yields the minimum number of lines, then run the balancing algorithm with that constraint pinned. Same trick browsers already use for column-count height balancing, applied one layer up.

One difference from text-wrap: balance

Everyone reaches for the text-wrap: balance comparison, including the Chrome release notes, and its a fair one. But theres a difference that matters.

Chromium caps text-wrap: balance at six lines. Past that it quietly gives up and wraps normally. Thats purely a performance guard: balancing text means iterating over candidate break points, and the cost climbs with the amount of text. Which is why text-wrap: balance is a headline tool and not a paragraph tool, and why slapping it on * is wasted work on everything that blows the cap.

There is no equivalent documented cap for flex-wrap: balance. That falls out of the fast path and the O(N log N) target rather than being an oversight. The set of flex items in a container is far smaller and far more tractable than the break opportunities in a block of prose, and the uniform-size shortcut covers most real usage anyway. So unlike its text cousin, you dont have to ration this one.

Doesnt mean spray it everywhere. On a container that never wraps its a no-op. On one that wraps into exactly two full lines, balance and wrap give you the same output. It earns its keep when the item count doesnt divide cleanly into the line capacity, which admittedly is most of the time.

The lab: try it yourself

Heres a live flexbox to push around. Change the item count, the container width, the base size, then hit the toggle to flip between wrap and balance and watch the last line snap into place.

If your browser supports it, youll see real balancing. If it doesnt, the toggle does nothing and the banner says so, which is a decent demo of the degradation story in its own right.

The setting to look at is 7 items, full width, uniform sizing. With wrap you get a clean first line, a clean second line, and one absurd item alone at the bottom. Flip to balance and the engine pulls items down until the lines even out. Now switch widths to “varied” and watch it behave differently: its equalising line length, so youll see rows with different item counts. Thats correct, not a bug.

Eight years, and it wasnt the syntax

This proposal is older than a lot of the CSS you use every day. The timeline is honestly the most interesting thing about the whole feature.

Tab Atkins opened csswg-drafts issue #3070 on 30 August 2018, titled “[css-flexbox-2] Add flex-wrap: balance;”. The framing was borrowed openly: developers want for flex lines what text-wrap: balance gives them for text lines. Tagged for Level 2. Then it sat there.

22 December 2024, Johannes Odland publishes “Web Wish 22: Flex-Wrap Balance”, part of an advent-calendar run of platform requests. His one-line description of the pain is still the cleanest Ive read: with flex: auto, “the items on that last row can end up awkwardly large.” He also raises something the spec still hasnt answered, which is whether authors should get to say which end of the cross axis absorbs the leftover space.

21 May 2025, Kilpatrick shows up on the issue with an implementation plan and publishes the flex-wrap-balance explainer: Knuth-Plass, the O(N log N) target, the uniform-size fast path, the bisection for column flexboxes. It also lists what got rejected along the way. A separate flex-line-style property. Direct break-after-item controls. And the perennial “just use multi-column”.

Chrome 149 puts it behind a developer trial on desktop and Android. 27 May 2026, the Intent to Ship goes out on blink-dev targeting Chrome 150 across desktop, Android and WebView, with full Web Platform Tests coverage confirmed. Chrome 150 hits stable on 30 June 2026, shipping it next to text-fit, animatable zoom, background-clip: border-area and rounded polygon(). Edge 150 follows two days later on 2 July, as Chromium downstreams do.

Eight years. And the thing that unblocked it wasnt a syntax fight, which is what usually holds these up. The syntax was basically settled back in 2018. What took the time was somebody working out an implementation fast enough to be worth shipping.

The honest support situation

Small warning before the good news. Ive seen a handful of posts this month calling flex-wrap: balance broadly supported and citing Chrome 114, Firefox 121, Safari 17.5. Those numbers belong to text-wrap: balance. Somebody copied them onto the wrong feature and the rest of the internet dutifully reposted it. Dont plan around them.

Heres the real position, straight from the Intent to Ship filed on 27 May 2026:

  • Mozilla (Gecko): no signal
  • WebKit: no signal
  • Interoperability risk: stated as significant — “other browsers do not implement”

So: Chromium-only, with neither Mozilla nor Apple saying anything either way. The argument Chrome makes in the same document is the one that actually justifies shipping anyway, and its that the thing degrades gracefully. A browser that doesnt understand balance throws the declaration out as invalid, falls back to whatever flex-wrap was before it, and you get an ordinary wrapping flexbox. Nothing breaks. Nobody sees an error. The layout is just a bit less tidy. Thats the exact adoption path text-wrap: balance took and it worked out fine.

So you can ship this today. No build step, no polyfill:

.cards {
  display: flex;
  flex-wrap: wrap;   /* everyone gets this */
  gap: 12px;
}

@supports (flex-wrap: balance) {
  .cards {
    flex-wrap: balance;  /* Chromium 150+ gets the tidy last row */
  }
}

The @supports wrapper isnt strictly necessary, mind you. Write flex-wrap: wrap balance as one declaration and browsers that cant parse balance will bin the whole thing, which is why splitting it in two, or gating with @supports, is the safer spelling. Costs you two lines.

What balance wont do for you

Three things to keep straight, since these are the questions Id expect in review.

It wont give you equal item counts per row. It equalises line length, and those two only coincide when your items are the same size. If what you actually need is “exactly four per row”, thats grid, not balance.

It wont take direction from your flex-grow values, at least not where you might expect. Adam Argyles write-up puts it well: “uneven wrapped rows still need the browser to own the balance.” Where the lines break is the engines call, made before growth gets applied. flex-grow still does its normal job inside each line, it just doesnt get a vote on the breaks.

And it wont touch DOM order. This one matters, and its where the feature is meaningfully less dangerous than grid-lanes masonry. Balancing moves line breaks, nothing else. Item 5 is still after item 4 and before item 6, in the DOM, in the accessibility tree, in tab order. No reading-order footgun. Nothing to audit.

So, ship it?

Ship it. flex-wrap: balance is a tiny feature and I like it more than its size warrants. It kills a real, daily, deeply boring annoyance without a new layout mode to learn, without a new mental model, and without the tradeoffs every workaround made us accept. One keyword.

Theres also something pleasing about where the fix came from. Your card grid gets sorted out by a typesetting algorithm Knuth published in 1981 for laying out TeX paragraphs, aimed at a different kind of box. Good ideas keep.

Chromium-only for now, silence from Firefox and Safari, and the worst case is exactly what you already have today. Two lines behind @supports and your last row quietly stops embarrassing you in Chrome.


References

Specification and standards process

Implementation and shipping

Documentation and community

People and prior art