DEV Community

loading...
Cover image for Flexbox: when negative margins save the day

Flexbox: when negative margins save the day

bcalou profile image Bastien Calou ・6 min read

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

The line wraps too soon

Do you notice how there is more space for the last item to go more to the right before the wrapping? Compare that to the space to the left of the first tag.

You could fix that by excluding the last item from the rule:

li:not(:last-child) {
  margin-right: 2em;
}
Enter fullscreen mode Exit fullscreen mode

But the problem is the same: every other item causes the line to wrap too soon.

The line still wraps too soon

Now the last tag can go farther to the right, but the other ones still cause 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;
}
Enter fullscreen mode Exit fullscreen mode

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:

The new line reveals the `margin-left` of the item

The new line reveals the `margin-left` of the item

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 */
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Blue borders applied on the list element reveal the trick

Blue borders applied on the `ul` element reveal the trick

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

The removal of the title margin (yellow area) would compensate for the new area inside the list

The removal of the title margin (yellow area) would compensate for the new area inside the `ul` (blue borders)

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;
}
Enter fullscreen mode Exit fullscreen mode

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 */
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

@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 /*
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
ul {
  --row-gap: 1em;
  --column-gap: 2em;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

pic
Editor guide