Last margin / padding collapsing in flexbox / grid layout

HtmlCssFlexboxCss Grid

Html Problem Overview


I have a list of items that I'm trying to arrange into a scrollable horizontal layout with flexbox.

Each item in the container has a margin left and right, but the right margin of the last item is being collapsed.

Is there a way to stop this happening, or a good workaround?

ul {
  list-style-type: none;
  padding: 0;
  margin: 0;
  display: flex;
  height: 300px;
  overflow: auto;
  width: 600px;
  background: orange;
}
ul li {
  background: blue;
  color: #fff;
  padding: 90px;
  margin: 0 30px;
  white-space: nowrap;
  flex-basis: auto;
}

<div class="container">
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
    <li>Item 4</li>
  </ul>
</div>

Html Solutions


Solution 1 - Html

Potential Problem #1

The last margin is not being collapsed. It's being ignored.

The overflow property applies only to content. It doesn't apply to padding or margins.

Here's what it says in the spec:

> 11.1.1 Overflow: the overflow > property > > This property specifies whether content of a block container element > is clipped when it overflows the element's box.

Now let's take a look at the CSS Box Model:

enter image description here

source: W3C

The overflow property is limited to the content box area. If the content overflows its container, then overflow applies. But overflow doesn't enter into the padding or margin areas (unless, of course, there is more content that follows).


Potential Problem #2

The problem with Potential Problem #1 is that it appears to fall apart outside of a flex or grid formatting context. For example, in a standard block layout, the last margin doesn't appear to collapse. So maybe overflow is permitted to cover margins / paddings, regardless of what it says in the spec.

div {
  height: 150px;
  overflow: auto;
  width: 600px;
  background: orange;
  white-space: nowrap;
}
span {
  background: blue;
  color: #fff;
  padding: 50px;
  margin: 0 30px;
  display: inline-block;
}

<div class="container">
    <span>Item 1</span>
    <span>Item 2</span>
    <span>Item 3</span>
    <span>Item 4</span>
</div>

Hence, maybe the problem is instead related to elements that are "over-constrained".

> 10.3.3 Block-level, non-replaced elements in normal > flow > > The following constraints must hold among the used values of the other > properties: > > margin-left + border-left-width + padding-left + width + > padding-right + border-right-width + margin-right = width of > containing block > > If width is not auto and border-left-width + padding-left + > width + padding-right + border-right-width (plus any of > margin-left or margin-right that are not auto) is larger than > the width of the containing block, then any auto values for > margin-left or margin-right are, for the following rules, treated > as zero. > > If all of the above have a computed value other than auto, the values are said to be "over-constrained" and one of the used values > will have to be different from its computed value. If the direction > property of the containing block has the value ltr, the specified > value of margin-right is ignored and the value is calculated so as > to make the equality true. If the value of direction is rtl, > this happens to margin-left instead > > (emphasis added)

So, according to the CSS Visual Formatting Model, elements may be "over-constrained" and, as a result, a right margin gets tossed out.


Potential Workarounds

Instead of margin or padding, use a right border on the last element:

li:last-child {
  border-right: 30px solid orange;
}

ul {
  list-style-type: none;
  padding: 0;
  margin: 0;
  display: flex;
  height: 100px; /* adjusted for demo */
  overflow: auto;
  width: 600px;
  background: orange;
}
ul li {
  background: blue;
  color: #fff;
  padding: 90px;
  margin: 0 30px;
  white-space: nowrap;
  flex-basis: auto;
}
li:last-child {
  border-right: 30px solid orange;
}

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
</ul>

Another solution uses a pseudo-elements instead of margins or padding.

Pseudo-elements on a flex container are rendered as flex items. The first item in the container is ::before and last item is ::after.

ul::after {
  content: "";
  flex: 0 0 30px;
}

ul {
  list-style-type: none;
  padding: 0;
  margin: 0;
  display: flex;
  height: 100px; /* adjusted for demo */
  overflow: auto;
  width: 600px;
  background: orange;
}
ul li {
  margin: 0 30px;
  background: blue;
  color: #fff;
  padding: 90px;
  white-space: nowrap;
  flex-basis: auto;
}
ul::after {
  content: "";
  flex: 0 0 30px;
}

ul::before {
  content: "";
  flex: 0 0 30px;
}

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
</ul>

Solution 2 - Html

Your problem is not the margin in itself. It's the scroll bar dimensioning only the visible content of the element.

One hack to solve it would be to create a visible element that occupies the margin

This solution handles this using a pseudo on the last child

ul {
  list-style-type: none;
  padding: 0;
  margin: 0;
  display: flex;
  height: 300px;
  overflow: auto;
  width: 600px;
  background: orange;
}
ul li {
  background: blue;
  color: #fff;
  padding: 90px;
  margin: 0 30px;
  white-space: nowrap;
  flex-basis: auto;
  position: relative;
}
li:last-child:after {
  content: "";
  width: 30px;
  height: 1px;
  position: absolute;
  left: 100%;
  top: 0px;
}

<div class"container">
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
    <li>Item 4</li>
  </ul>
</div>

Solution 3 - Html

You can set width and overflow on the div container, and set display: inline-flex rather than flex on the ul, so that the size of the flex box will be calculated based on the items inside, and all padding and margin will apply without any issues.

.container {
  width: 600px;
  overflow: auto;
}

.container ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: inline-flex;
  background: orange;
}

.container li {
  padding: 60px;
  margin: 0 30px;
  white-space: nowrap;
  background: blue;
  color: #fff;
}

<div class="container">
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
    <li>Item 4</li>
  </ul>
</div>

Solution 4 - Html

See the solution. I remove white-space, flex-basis and margin, to provide you a pure flexbox solution.

It relays on flex-flow: row(horizontal), justify-content: space-around (your margin) and no more!! The width is changed to 1200px since the padding of 90px set the total width of the boxes more than your 600px (defined in your snippet).

ul {
  list-style-type: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-flow: row ;
  justify-content: space-around;
  height: 300px;
  overflow: auto;
  width: 1200px;
  background: orange;
}
ul li {
  background: blue;
  color: #fff;
  padding: 90px;
}

<div class"container">
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
    <li>Item 4</li>
  </ul>
</div>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKevin WilsonView Question on Stackoverflow
Solution 1 - HtmlMichael BenjaminView Answer on Stackoverflow
Solution 2 - HtmlvalsView Answer on Stackoverflow
Solution 3 - HtmlStickersView Answer on Stackoverflow
Solution 4 - HtmlMarcos Pérez GudeView Answer on Stackoverflow