I recently took the time to learn how Heydon Pickering's Flexbox Holy Albatross works so I could use it on my own site in lieu of container queries. In the process of implementing the technique, I realized that I would sometimes need to force a particular child in the layout to always span the full width of its row rather than getting pulled in alongside other siblings. To accomplish this, I implemented a variation of the Holy Albatross technique that allows you to control the number of columns at either the layout or child level.

Recap: What Is the Flexbox Holy Albatross?

If you're already familiar with the technique and how it works, feel free to skip ahead to the section on controlling the number of columns.

"Holy Albatross" is a term coined by Heydon Pickering to describe a CSS technique where a flex container automatically switches from a multi-column layout to a single column at a target container breakpoint. This method was later incorporated into Every Layout and dubbed The Switcher. The code below is a slightly modified version of the one in Heydon's article; it includes some auxillary variables to clarify what's going on, but note that those intermediate variables are not necessary:

.switcher {
  display: flex;
  flex-wrap: wrap;
  --sign-flag: calc(var(--breakpoint, 40rem) - 100%);
  --multiplier: 999;
}
.switcher > * {
  min-width: 33%;
  max-width: 100%;
  flex-grow: 1;
  flex-basis: calc(var(--sign-flag) * var(--multiplier));
}

The layout specifies a target container breakpoint (--breakpoint) at which it should switch from a multi-column arrangement to a single column; this value can be overridden as needed. How and when the layout wraps its children is determined by the combination of min-width, max-width, and flex-basis set on the flex children. In this example, each child is given a min-width of 33%, yielding three columns at most. Each child is also given a max-width of 100%, meaning that when the layout width becomes sufficiently narrow, each flex child will occupy its own row.

The trick relies on the fact that if a flex item's flex-basis is negative or zero, then its min-width will take effect. Conversely, if the flex-basis is positive and exceeds the child's max-width, then the max-width will win out.

In practice, this "switching" behavior is achieved by subtracting 100%—the current width of the layout—from the target breakpoint. This produces a positive, negative, or zero value:

.switcher {
  --sign-flag: calc(var(--breakpoint, 40rem) - 100%);
}

Or, in simpler terms:

signFlag = breakpoint - 100%

This variable is:

  1. Positive when 100% < breakpoint (the container is narrower than the breakpoint).
  2. Negative when 100% > breakpoint (the container is wider than the breakpoint).
  3. Zero when 100% === breakpoint (the container's width matches the target breakpoint).

We then multiply this sign flag by an arbitrarily large value (999) to scale it to either extreme—very negative or very positive—and apply this as the flex-basis for each child:

.switcher > * {
  flex-basis: calc(var(--sign-flag) * var(--multiplier));
}

When the calculation yields a negative or zero flex basis, each child spans three columns (min-width: 33%). When the calculation yields a positive flex basis, each child spans the full width of the layout (max-width: 100%). The table below explores a few scenarios:

Container width Breakpoint flex-basis min-width max-width Columns
41rem 40rem -999rem 33% (13.53rem) 100% (41rem) 3
40rem 40rem 0rem 33% (13.20rem) 100% (40rem) 3
39rem 40rem 999rem 33% (12.87rem) 100% (39rem) 1

It's important to emphasize that this entire technique relies on the container width rather than a viewport width; the latter is not very useful when you want to create a truly reusable container that adjusts its behavior based on the context in which it is rendered.

Controlling the Number of Columns

In the original article, Heydon used the example of a three-column layout, which as we saw is achieved by setting min-width: 33% on the flex children. What if we could abstract this into a custom property specifying the number of desired columns in the layout and derive the percentage from that value? Perhaps something like this:

.switcher > * {
  min-width: calc(100% / var(--columns, 3));
  /* all other CSS from before */
}

This layout has 3 columns per row by default, but we can override that value later.

If we add gaps to our layout, we'll need to factor those into the min-width calculation:

.switcher {
  --gap: 1rem;
  display: flex;
  flex-wrap: wrap;
  gap: var(--gap);
}
.switcher > * {
  --num-gaps: calc(var(--columns, 3) - 1);
  min-width: calc(calc(100% / var(--columns, 3)) - var(--num-gaps) * var(--gap));
}

More generally, a gapped layout with n columns has n - 1 gutters. We multiply this by the size of each gap (--gap) to get the final, accurate min-width for each flex child.

Here's a demo of what the default UI might look like:

Finally, we can set up utility classes to switch the number of columns at either the layout level or on any individual child:

.col-1 {
  --columns: 1;
}
.col-2 {
  --columns: 2;
}
/* etc */

When set at the layout level, this dictates the maximum number of columns globally:

<!-- An auto-wrapping flex layout with 2 columns max -->
<div class="switcher col-2"></div>

But where it gets interesting is if we set it on a specific child:

<div class="switcher">
  <div></div>
  <div></div>
  <div></div>
  <div class="col-1"></div>
  <div></div>
  <div></div>
</div>

Rather than defining a column span like in a traditional grid layout, this utility class tells the parent how many columns it should create on that child's row. In this case, even though the layout allows for a maximum of three columns, the fourth child will always be on its own row (--columns: 1):

All other children will obey the default number of columns, unless they have room to grow (like that last row, where only two children can fit).

That's it! This modified version of the Flexbox Holy Albatross technique allows you to create auto-switching layouts with granular control over the number of columns on any particular row.

Attributions

Social media preview: Photo by Karin Hiselius (Unsplash).