Gap management with flexbox is not as easy as it seems. Here is a simple trick I've been using a lot lately.
The problem
This is our HTML for this demo:
<article>
<h1>Hello World</h1>
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
<li>Front-end dev</li>
<li>Web</li>
</ul>
<p>Lorem ipsum...</p>
</article>
It's an article with a list of tags. With some basic CSS, here's what it looks like.
We want the list of tags to be a flex container, with the possibility to wrap. Here we go!
ul {
display: flex;
flex-wrap: wrap;
}
li {
margin-right: 2em;
}
As you can see, I also set some space after each <li>
element, 2em
being equal to 32px
for common text (with the advantage of being responsive to the user preferences and the font-size of the item itself).
And here's the result:
It may seem good enough, but the devil is in the details.
Look at the bottom right corner of the card: I made it resizable so you can simulate a browser resize.
There are two main issues. Can you spot them?
The horizontal issue
The first problem is that because of its right margin, the last items goes to the second line too soon.
You could fix that by excluding the last item from the rule:
li:not(:last-child) {
margin-right: 2em;
}
But the problem is the same: every other item causes the line to wrap too soon.
Well, if margin-right
is not good enough, what about margin-left
? Let's try this on every item — except the first one, which should not be preceded by any space.
li:not(:first-child) {
margin-left: 2em;
}
Is it better this time? Take a moment and try to guess what issue it might cause.
Because there is no margin-right
anymore, the line wraps exactly when it should. But now our problem is elsewhere:
We can't complain. We told CSS that each item except the first one should have a left margin, and that's what happens.
How nice would it be to exclude every item that is the first one of its row! But there is no magic selector like this:
li:not(:first-flex-row-item) /* Does not exist */
Such hypothetical selectors could cause a CSS circular dependency. For example, I could say that the first item of a row has a smaller font-size. That could cause the item to go back to the previous line (because of it smaller size), and then it wouldn't be targeted by the selector anymore, and regain its original size, and go back to the second line and... 🤯
Circular dependencies one of the main reasons we don't have container queries yet. But that's another topic.
The negative margin trick
So here's how you do it: first off, every item will get a margin-left
.
li {
margin-left: 2em;
}
We now have to get rid of the space to the left of the first item, and we can do that with negative margins.
Negative margins are not considered a good practice, and I think that you should avoid them whenever possible, because they can make your code's logic harder to understand.
That being said, they are allowed by the w3c and offer a really good browser support.
And in our case, they save the day:
ul {
display: flex;
flex-wrap: wrap;
margin-left -2em;
}
li {
margin-left: 2em;
}
What about the vertical axis?
Well guess what, it's the same thing!
You can't target every item except the ones that are on the first line.
So you have to give everyone a margin-top.
li {
margin-left: 2em;
margin-top: 1em;
}
This causes the whole list to appear lower that what we want.
We could remove the margin-bottom: 1em
from the title tag to compensate.
But I always try to keep my elements independent of the context. The list could appear below another element at some point. Or a title could be followed by something that is not a list.
You know, component driven development, design system and all that jazz.
So we just have to use the same trick and apply a negative margin to our list:
ul {
display: flex;
flex-wrap: wrap;
margin-left -2em;
margin-top: -1em;
}
And here's our final version. It works on every browser correctly supporting this flexbox configuration, including IE11.
What about the gap
property?
Posts like this one will become irrelevant once the CSS gap
property is widely supported.
But that's not the case yet. At the time of writing, its browser support is only 70%. Not that great, compared to the 99% support of flexbox itself — has Safari really become the new IE?
Other modern browsers should show you the same result with the following code, without tricks!
ul {
display: flex;
flex-wrap: wrap;
gap: 1em 2em; /* row-gap + column-gap */
}
/* No more styles on the items */
The sad part is that you can't even detect the support of this property. Consider the following code:
@supports(gap: 1em 2em) {
/* Cancel the tricks and do the right thing */
}
@supports
queries allow you to apply rules only if the browser understands what's inside the parenthesis.
The problem here is that gap
is also a property used on grids, with a much better support of 92%. But that does not mean that the property will work for flexbox.
Here's the issue being discussed by the CSS Working Group.
In the meantime, it's negative margins all the way.
Now with variables ✨
We can improve our code and make it more generic if we need. I like to separate my BEM/semantic CSS from my utility classes, so I will create a class called u-flex
.
I'm not a big fan of having style-oriented classes in my HTML
, so I would probably use a SASS mixin instead, but you get the idea.
Let's use CSS variables, which have a very decent support (95%). In CSS, you can get the opposite of a value by multiplying it by -1
. Here is an example:
div {
--size: 2em;
width: calc(-1 * var(--size)); /* -2em /*
}
So here's our utility class:
.u-flex {
display: flex;
flex-wrap: wrap;
margin-top: calc(-1 * var(--row-gap));
margin-left: calc(-1 * var(--column-gap));
}
.u-flex > * {
margin-top: var(--row-gap);
margin-left: var(--column-gap);
}
I like to use the "direct child of any type" selector (> *
) with flexbox and grid. It relates very well to the parent/child relationship of these features and will work every time.
And here's how you would use it:
<ul class="u-flex">
<li>HTML</li>
<li>CSS</li>
<li>JavaScript</li>
<li>Front-end dev</li>
<li>Web</li>
</ul>
ul {
--row-gap: 1em;
--column-gap: 2em;
}
The power of CSS variables allows us to define different gaps for each targeted element. We could even define default values for the whole document:
:root {
--row-gap: 1em;
--column-gap: 2em;
}
This way, we only have to change the variables locally when we need to have a different gap value.
Bad practices
We all know many bad practices: !important
is another one that comes to my mind. But like negative margins, it also has some relevant use cases.
This negative margins trick is a reminder to me: things you learned to avoid might come in handy some day. It all depends of the context.
Top comments (0)