Flexbox Fun Facts

This post is brought to you by “I am procrastinating other stuff by doing some long overdue maintenance on my blog”. Mainly, I finally replaced the old float-based layout from the random Hugo theme I forked, which I had been keeping just because it wasn’t broken, with flexbox, so that I could more easily tweak some other things. If things look broken, you may need to force-refresh or clear your cache, and on the off chance things look mostly the same but you feel like something about the layout feels subtly different, that’s what’s up.

While making these changes, I ended up digging through the flexbox spec to debug an issue and learned some interesting things. (This and other links in this post are permalinks to the November 2018 spec, which I believe is the most recent official version as of time of writing, but it’s nearly three years and there have been quite a few changes in the “editor’s draft”. Also, this post is not a flexbox tutorial and will not make sense if you are already familiar with flexbox.)

  1. If the values of flex-grow sum to less than 1, only that fraction of all the free space will be allocated. Most people will never encounter this unusual behavior because flex-grow is usually a nonnegative integer. The reason for it is basically to allow flex-grow to animate smoothly.

  2. You can collapse flex items with visibility: collapse, causing them to not render except that their cross size is still taken into account. (The cross size is the dimension perpendicular to the flex main axis, i.e. the height of a flex row and the width of a flex column.) This is mainly useful when the flex item might be dynamically collapsed and uncollapsed, and you want to limit the ripple effects of that on the layout; the spec link has a more complete example.

  3. (The previous two fun facts were just window dressing, this is the fun issue I was actually investigating…) In browsers today, in a flex row, width counts towards the max-content of the flex container, but flex-basis doesn’t.

    As a concrete example, many browsers currently render the inner <div> below as actually 200em wide, way overflowing the container:

    However, they render the inner <div> below as just the width of “hello”:

    The top StackOverflow answer on flex-basis vs width argues that, according to the spec, there should be “no difference” between width and flex-basis under some simple preconditions (including, of course, that the flex direction is row or row-reverse — for reasons we’ll explore, I don’t think this claim is true, but I am not confident that I’m right or that this isn’t the result of spec changes since the answer). However, nested flex containers are quite buggy in browsers, with several important aspects ironically only functioning correctly in Edge. Here’s the most recently updated Firefox bug and Chromium bug I found. Why do width and flex-basis behave weirdly in nested flex containers? I could not find any resources that seemed more worth investigating than the W3C spec itself.

    Authors writing web pages should generally be served well by the individual property descriptions, and do not need to read this section unless they have a deep-seated urge to understand arcane details of CSS layout.

    Crudely, when items don’t have explicit sizes, the flexbox layout algorithm starts by sizing the flex items “under a max-content constraint”, which roughly means that it lays the items out in an imaginary void where they’re allowed to take up as much space as they want, but won’t take up any extra space for no reason. For example, text will all go on a single line and never wrap. Then it uses the size of that imaginary laid-out item as a starting point to flex.

    You can actually replicate the imaginary void layout with width: max-content, and indeed, these two examples show the same diverging behavior as the two previous examples:

    In our case, the flex item is itself a flex container, so we have to size it “under a max-content constraint”. As far as I can determine, this is calculated based on § 9.9.1. Flex Container Intrinsic Main Sizes, which has an algorithm that’s incredibly convoluted, for what seems to be the same reason Fun Fact #1 is true: to ensure that changing flex-grow or flex-shrink smoothly also changes calculated dimensions smoothly. Based on the expository green note, our case seems to boil down to wanting the “max-content contribution” of the single flex item, which is then defined in § 9.9.3. Flex Item Intrinsic Size Contributions:

    The main-size max-content contribution of a flex item is the larger of its outer max-content size and outer preferred size (its width/height as appropriate) clamped by its flex base size as a maximum (if it is not growable) and/or as a minimum (if it is not shrinkable), and then further clamped by its min/max main size.

    The plot thickens. This is one of very few references to “preferred size” — that is, the width or height (of a flex row item or flex column item, respectively) — rather than “flex base size” in the flexbox spec; the latter is what flex-basis controls, but often defaults to width or height, and is why they’re so similar. As far as I understand:

    • In our first examples, the innermost <div>s have preferred sizes of 200em. That’s also their flex base size, so that’s their main-size max-content contribution.
    • In our second examples, the innermost <div>s do not have preferred sizes, so we take their max-content size, the width of the text inside them. Because they are shrinkable (flex-shrink defaults to 1), we don’t clamp by their flex base size. So their main-size max-content contribution is actually the width of their text.

    I don’t know why the spec is like this exactly and could easily have misunderstood or skipped something, but this logic seems to suggest that our two <div>s should indeed be laid out differently. (This is arguably different from the reported browser bugs and examples, where the inner <div> has the expected size but the middle <div> is sized strangely, and where I think the § 9.9.1 algorithm should just be summing up the flex base sizes because the items are inflexible, but doesn’t appear to be.)

    Anyway: because flex-grow defaults to 0, it makes sense that our narrow second examples stay narrow; but flex-shrink does default to 1, so in our first examples, there’s the remaining question of why the inner <div>s don’t shrink to fit in the outer <div>. While the algorithm for resolving flexible lengths is pretty intimidating, the important part is what it considers a “min/max violation”: when the width passes the “min/max main size”, which in our case is min-width.

    Now (and these are from the CSS Sizing module rather than flexbox), the default value of min-width is auto, which is actually just 0 for most vanilla HTML elements; but § 4.5. Automatic Minimum Size of Flex Items overrides that definition for flexbox items to… yet another complicated amalgamation of conditions, but in our case I believe it’s the “min-content main size” of the item. Since the item is itself a container, we’re sent back to § 9.9.1 and § 9.9.3 to read the min counterparts to the metrics we previously encountered, and want to calculate the “min-content contribution” of the single innermost flex item. But here we just end up using the preferred size, 200em, as the min-content contribution again.

    Therefore, it makes sense that none of the <div>s shrink in the first example in each pair.

  4. But I actually glossed over the most fun fact of all: we saw two inner <div>s, one way too wide and the other way too narrow, but there’s a good chance neither behavior is what we want. Instead, we want the <div> to “start out too wide”, but flex by shrinking to the width of the outer <div>. Isn’t flexing the point of flexbox?

    One solution is to just set the width of the middle container.

    Another is to override the flex-item min-width.

    Setting the flex-basis of the middle container does not work, because min-width is “stronger”.

Here is a quick CodePen with all the examples in this post.

(note: the commenting setup here is experimental and I may not check my comments often; if you want to tell me something instead of the world, email me!)