Last updated:

CSS Columns Are the Layout Method Everyone Forgets

Two lines get you newspaper columns. The third line is the one nobody mentions.

Saw a post from Theo Soti this week making the case for the CSS columns property. It’s a good use case for multi-column, newspaper-style text layout. No grid, no flexbox, two lines of CSS. He’s right that it’s underused, I’m not sure I’ve reached for it more than a handful of times since finding out about it, and it’s normally as a last resort when something that more ingrained options like flex or grid don’t seem to do the trick.

Given this lack of usage but wanting to dive in a bit more, I built the two-line version from the original, and then some more to figure out what the downsides are. There must be some big ones, right? For it to somewhat fly under the radar?

So, the two lines. Pretty simple stuff.

.container {
  columns: 200px 3;
}

That’s it. For the happy, easy-breasy path anyways. columns is a shorthand for column-width and column-count, really reading as “at least 200px per column, and don’t exceed 3 columns”, and then it’s left in the browsers capable hands to figure out how many actually fit. Sprinkle in some column-gap and column-rule for spacing and a divider, and you’ve got clean readable columns for biographies, long-form text, feature-lists…anything that’s really just a block of text.

If we want something to break out of those columns and sit above them, like an introduction paragraph, we can use column-span to get all of that just as easily.

  .text-columns {
    columns: 200px 3;
    column-gap: 1.5rem;
    column-rule: 1px solid #ccc;
  }

  .text-columns .intro {
    column-span: all;
  }

This line spans all columns first, then the rest of the content flows into separate columns below it.

Without changing the parent column setup, one child can opt out of the column flow and sit full-width as an intro.

This works well for lead paragraphs, section labels, and short contextual notes before dense text content.

After that line, normal column flow resumes and the browser continues distributing paragraphs by available space.

The missing piece

Using this for getting blocks of text into columns is perfect, but the moment we put anything more structured inside it, like a card, an image, a bordered note, it falls over completely. The browser doesn’t know, or more precisely, care, that your .card element is a single visual component. It just keeps flowing content down the current column until it runs out of vertical space, and then starts the next one. If that happens when it’s mid-element, then we get the browser-equivalent of a shrug, and the break happens.

So how do we get around it? Well, luckily, we don’t need some odd hack or weird workaround, as I’d fully expect in a situation like this. The fix is one more line, but this time on the child (we’re using a used-so-much-its-boring card element) this time:

@scope (.text-columns) {
  .card {
    break-inside: avoid;
  }
}

Look at the example below and watch a card get sliced when we run out of vertical space. That’s where the separation between the “text layout trick” and the “general-purpose layout tool” is. columns is genuinely great at the first thing, but it’ll happily let you shoot yourself in the foot and cause a scene if you go in blindly. Yes, the learning curve is less than using flex or grid for creating columns, but they are definitely more flexible than columns and don’t require unrelated caveats like break-inside. It’s not something naturally found while learning about columns until you had that specific issue.

Same three cards, same column-count: 2. The only difference is break-inside: avoid on the right.

01

Fetch on mount

Load data when the component first renders.

02

Skeleton loaders

Hold the layout steady while the request is in flight.

03

Error boundaries

Catch a failed request without taking the whole view down.

01

Fetch on mount

Load data when the component first renders.

02

Skeleton loaders

Hold the layout steady while the request is in flight.

03

Error boundaries

Catch a failed request without taking the whole view down.

Where this actually fits

Using columns is best when using genuinely text-heavy content, like biographies, FAQ answers, footer link lists on top of those mentioned at the start of the post. The moment we start using components in them, that’s most likely a good time to reconsider using columns and go for an alternative, like flex or grid. This doesn’t, and never will replace them, it’s a third, narrower tool that’s super easy to forget exists.

Quick note on support here, since it’s not a single “check one number” situation here. columns itself has been safe for years now, it’s actually break-inside that needs the check. The same property also handles page breaks, and does its own thing inside flex and grid items too. If you go searching and the first result looks reassuring, there’s half a chance that it’s not talking about what happens when we’re using a multi-column layout. Worth checking if you’re looking at the right behaviour before you trust it. column-span is worth a glance too if you’re using the intro trick from earlier, just to be safe.

As I noted at the beginning of this post, I’ve reached for columns only a few times. If it’s used more and break-inside turns out to have a few of its own edge cases, like widows, orphans etc, then I’ll update this post further to include how they could, or if they could, be dealt with, or if that’s the death knell on it.