Unholy Albatross

June 29, 2020

Updated July 16, 2020: new and improved in Container Query Round 2.

The Holy Albatross is a clever bit of CSS by Heydon Pickering to responsively stack all columns into rows based on container size, without an intermediary step where some of the columns becomes rows but not others.

Using the following property:

flex-basis:calc((breakpoint - 100%) * 999); flex-grow:1

...allows the column to switch between negative and positive flex-basis (at a minimum 999px wide due to 1px * 999), which combined with default flex-shrink value of 1 allows the column to expand to full width as soon as 100% container width falls below the breakpoint.

Flex-grow is used to redistribute free space.

Note: Negative flex-basis is supposed to be invalid, thus defaulting to auto. However it looks like it's behaving as 0 instead. This helps us because column content size doesn't interfere with flex-grow, but I'm not sure if this behavior is a bug or not.

Width problem

Problem is, it's very difficult to set width to your non-stacked column, because the width is taken over by the hack.

One suggestion by Heydon is to use flex-grow, the problems are:

  1. It is a very unintuitive way to set width - for a 3 column layout, you want 1 column to be 50% wide, you have to set flex-grow to 2.333
  2. You have to know the number of total columns and set other columns' flex-grow value accordingly

The other method is to use min-width and max-width, as shown by this codepen. I don't believe max-width: 100% is needed as anything larger than 100% will be changed to 100% due to flex-shrink, so really we're dealing with min-width.

The problem with this method is that we have to set min-width for all columns, or else flex-grow will take over and expand the column beyond the min-width we've set.

None of this is fun.

I poked around a bit and I found that you can have your cake and eat it too...

Unholy Albatross, harnessing the black magic of max()

Presenting three variants of the solution:

Unholy Albatross #1: first iteration demonstrating the basic concept.

.container {
  display: flex;
  flex-wrap: wrap;
  --breakpoint: 40rem;
  --modifier: calc((var(--breakpoint) - 100%) * 999);
}

.container > * {
  flex-grow: 1;
  
  flex-basis: max(var(--width, 0px), var(--modifier));
  max-width: max(var(--width, none), var(--modifier));
}

.container > *:first-child {
  --width:33.33%; /* percentage width doesn't play nice if you add gutters */
}

How does it work?

Simply by exploiting the max() CSS function and clever fallbacks.

For flex-basis we pick the larger of --width and --modifier, when --modifier is negative, we will use --width for flex-basis, and if --modifier is larger, we will use --modifier for flex-basis.

If --width is undefined, we set a fallback of 0 to let flex-grow divide the remaining space evenly.

To prevent column from growing larger than width set, we set max-width using the same max() calculation as flex-basis, except this time instead of 0 for fallback, we use none to allow the column to grow.

And there it is.

Unholy Albatross #2: adding gutter support with fractional (percentage) width.

.container {
  display: flex;
  flex-wrap: wrap;
  
  --gutter: 2rem;
  margin: calc(var(--gutter) / 2 * -1);
  
  --breakpoint: 40rem;
  --modifier: calc((var(--breakpoint) - 100%) * 999); 
}

.container > * {
  flex-grow: 1;
  
  flex-basis: max(var(--width, 0px), var(--modifier));
  max-width: max(var(--max-width), var(--modifier));
  
  margin: calc(var(--gutter) / 2);
  
  --width: calc(99.999% * var(--w, 0) - var(--gutter));
  --max-width:  calc(99.999% * var(--w) - var(--gutter));
}

.container > *:first-child {
  --w:1/3;
}

.container > *:last-child {
  --w:1/5;
}

Here a variant to utilizing percentage width and gutter, improved in Unholy Albatross #3.

Clever fallback is used again to set --width to 0 if --w is undefined, just like before.

As for --max-width, if --w isn't defined we will allow the property assignment to fail, thus leaving max-width to the initial value of none, just as we wanted.

New: Unholy Albatross #3: supports gutters, fixed and fractional width.

.container {
  display: flex;
  flex-wrap: wrap;
  
  --gutter: 2rem;
  margin: calc(var(--gutter) / 2 * -1);
  
  --breakpoint: 40rem;
  --modifier: calc((var(--breakpoint) - 100%) * 999); 
}

.container > * {
  flex-grow: 1;
  
  flex-basis: max(var(--width, var(--auto-width)), var(--modifier));
  max-width: max(var(--width, var(--max-width)), var(--modifier));
  
  margin: calc(var(--gutter) / 2);
  
  --auto-width: calc(99.999% * var(--w, 0) - var(--gutter));
  --max-width:  calc(99.999% * var(--w) - var(--gutter));
}

.container > *:first-child {
  --w:1/2;
}

.container > *:nth-child(2) {
  /* shrink-wrap */
  --w:0;
  min-width: min-content;
}

.container > *:last-child {
  --width:100px;
}

As I posted the previous 2 variants, I thought to myself - "there has to be a way to use both fixed and percentage width."

Turns out we can!

By adding an intermediary --auto-width property and increasing the fallback chain, we can now use both fractional width with --w and fixed width with --width

Bonus: You can also shrink-wrap the column by setting --w to 0, and set min-width: min-content or min-width: fit-content depending on your needs.