flex gap polyfill

May 21, 2020

I wrote this post for my newsletter, sign up here to get emails in your inbox.


Note: This is not a direct tutorial on how to implement a polyfill, these are my raw notes for what the polyfill in React UI should do

 

The specification for gap in flexbox was introduced to the CSS working draft in September 2017.

While margin and padding can be used to specify visual spacing around individual boxes, it’s sometimes more convenient to globally specify spacing between adjacent boxes within a given layout context

For example, in this form, we want fieldsets to have the same gap between them.

form with gap

But, browsers don’t support it yet because it’s still a working draft (except firefox)

Different libraries have polyfilled this feature in different ways.

Let’s start with a dummy example which puts 3 elements in a flex container.

<div class="flex-gap-10">
  <div class="box">one</div>
  <div class="box">two</div>
  <div class="box">three</div>
</div>

 

Naive margin approach

The naive approach is to add margin to the direct children of the flex element:

.flex-gap-10 > * {
  margin-bottom: 10px;
}

naive implementation

With this approach, you get margin even on the last element which increases the size of the container, which is not something we want.

We can remove this margin by adding an additional rule:

.flex-gap-10 > *:last-child {
  margin-bottom: 0;
}

naive flex with last child fix

 

Specificity collisions with children

The problem with this approach arises when a child also has margin on it.

<div class="flex-gap-10">
  <div class="box">one</div>
  <div class="box margin-bottom-20">two</div>
  <div class="box margin-bottom-20">three</div>
</div>
.flex-gap-10 > * {
  margin-bottom: 10px;
}
.flex-gap-10 > *:last-child {
  margin-bottom: 0;
}
.margin-bottom-20 {
  margin-bottom: 20px;
}

flex conflict

There are 2 things to talk about here -

  1. The last element did not get any margin at all.

    This is because .flex-gap-10 > *:last-child has more specificity (0.2.0) than .margin-bottom-20 (0.1.0), which means it will override the other regardless of the order in which they appear in the stylesheet.

    One way to solve this problem is to use the lobotomised owl selector * + * along with margin-top. This has a specificity of 0.1.0 and only depends on the order in which it appears now.

    .flex-gap-10 > * + * {
      margin-top: 10px;
    }
  1. The gap between the 2nd and 3rd box is 20px, the margin on the child overrides the margin inserted by the parent.

    Even though .flex-gap-10 > * and .margin-bottom-20 have the same specificity (0.1.0), .margin-bottom-20 is applied simply because it appears later in the stylesheet. The outcome changes if you put it on top.

    The order of classes is an implementation detail that the user of these classes should not be aware of - which makes this a unpredictable API.

 

We want an API that has a predictable promise and does not depend on the implementation detail or the order in which it appears.

First, we must answer the question of what is the promise?

 

Expectation

<div class="flex-gap-10">
  <div class="box">one</div>
  <div class="box margin-bottom-20">two</div>
  <div class="box">three</div>
</div>

When we talk about the gap between 2nd and 3rd box, should it

  1. be ignored and the layout container gets precedence? = 10px
  2. or override the margin from parent? = 20px
  3. or both styles be respected and get added? = 30px

Looking at various implementations,

  1. Tailwind CSS chooses the 1st option with it’s space-y-{amount} utility
  2. React UI chooses the 2nd option and treats this API as a child overriding what the parent container has set.
  3. Braid design system chooses the 3rd and adds them up by wrapping up the children in an element with padding.

Which one is correct is a subjective choice. An argument can be made for any of these based boundaries of responsibilities or ease of use.

Adam Wathan explained their choice in the pull request which adds space-y-{amount} utility: #1584. Mark Dalgleish (works on Braid), explained his preference for the 3rd option in this twitter thread.

 

Specification update

The latest edition of the working draft (updated 21 April, 2020) adds an example that can serve as a tie breaker -

Note: The gap property is only one component of the visible “gutter” or “alley” created between boxes. Margins, padding, or the use of distributed alignment may increase the visible separation between boxes beyond what is specified in gap. gutter gap and margin

This example shows that the working draft prefers the 3rd option from above.

The way to implement the 3rd option is to wrap each child in an element, which makes the implementation fairly straight forward:

.flex-gap-10 > .flex-child:not(:last-child) {
  margin-top: 10px;
}
<div class="flex-gap-10">
  <div class="flex-child-wrapper">
    <div class="box">one</div>
  </div>
  <div class="flex-child-wrapper">
    <div class="box margin-bottom-20">two</div>
  </div>
  <div class="flex-child-wrapper">
    <div class="box">three</div>
  </div>
</div>

flex final

If you are building a Flex/Stack in a component based framework, then you can hide this implementation detail by wrapping children under the hood - which is great because when the feature makes it into modern browsers, you can remove that detail without breaking the API or the pages it is used in.


Want articles like this in your inbox?
React, design systems and side projects. No spam, I promise!