17

I'm trying to nest two child elements in a wrapper which specifies side margins so there's space between its contents and the sides of the screen when the display is narrow and a max-width for when the display is wide.

The second child has some overflow which should be visible while the first child should stay strictly within the wrapper's content box. With the first child removed, the second child behaves as desired. When I add in the first child though, it seems to completely ignore the wrapper's margins, stretching the wrapper's content box and breaking the second child along with it.

Applying overflow: hidden to the wrapper fixes the margin problem but clips the second child. Applying the margins to the first child didn't make it collapse with the parent since it's in a new block formatting context.

The only workaround I've found so far would be to do:

.wrapper {
    > * {
        margin-left: 1.5rem;
        margin-right: 1.5rem;
    }
}

and increase the max-width of the wrapper by 3rem but I was hoping there was some solution that didn't require me to shift the margin from the wrapper to its children.

https://codepen.io/HybridCore/pen/jjoWmd

body {
  background-color: #000;
  color: #FFF;
  display: flex;
  justify-content: center;
}

.wrapper {
  margin: 0 1.5rem;
  max-width: 40rem;
  width: 100%;
}

.fit_content_box {
  display: flex;
  align-items: center;
}

.L {
  min-width: 0;
  flex: 1 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.R {
  margin-left: 1rem;
  height: 1rem;
  width: 1rem;
  background-color: #FFF;
}

.overflow {
  display: flex;
  justify-content: space-between;
}

.overflow>div {
  width: 0;
  display: flex;
  justify-content: center;
}
<body>
  <div class="wrapper">
    <div class="fit_content_box">
      <p class="L">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
        dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>

      <div class="R"></div>
    </div>

    <div class="overflow">
      <div>
        <p>0</p>
      </div>
      <div>
        <p>12</p>
      </div>
      <div>
        <p>24</p>
      </div>
    </div>
  </div>
</body>
jhpratt
  • 6,841
  • 16
  • 40
  • 50

2 Answers2

6

You mainly have two issues:

  1. You are setting width:100% to the wrapper and this doesn't account for margin so you will logically have overflow and since the body is a flex container with justify-content:center the margin will overflow equally from both sides that's why you think it's not applied.
  2. You are facing the min-width constraint of flexbox which is forcing you to set width:100% thinking it's the good solution. This same constraint is also preventing the element from shrinking lower than the 100% you specified (related: Why is a flex item limited to parent size?)

To fix this you need to remove width:100% from wrapper and consider min-width:0 instead. You can also remove the min-width applied to .L and you need to consider flex-shrink:0 on .R (or replace its width by min-width)

body {
  background-color: #000;
  color: #FFF;
  display: flex;
  justify-content: center;
}

.wrapper {
  margin: 0 1.5rem;
  max-width: 40rem;
  min-width:0;
}

.fit_content_box {
  display: flex;
  align-items: center;
}

.L {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.R {
  margin-left: 1rem;
  flex-shrink:0;
  height: 1rem;
  width: 1rem;
  background-color: #FFF;
}

.overflow {
  display: flex;
  justify-content: space-between;
}

.overflow>div {
  width: 0;
  display: flex;
  justify-content: center;
}
<body>
  <div class="wrapper">
    <div class="fit_content_box">
      <p class="L">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure
        dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>

      <div class="R"></div>
    </div>

    <div class="overflow">
      <div>
        <p>0</p>
      </div>
      <div>
        <p>12</p>
      </div>
      <div>
        <p>24</p>
      </div>
    </div>
  </div>
</body>

If you want the element to remain at least equal to max-width when there is a small amount of text add flex-grow:1:

body {
  background-color: #000;
  color: #FFF;
  display: flex;
  justify-content: center;
}

.wrapper {
  margin: 0 1.5rem;
  max-width: 40rem;
  min-width:0;
  flex-grow:1;
}

.fit_content_box {
  display: flex;
  align-items: center;
}

.L {
  flex-grow:1;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.R {
  margin-left: 1rem;
  flex-shrink:0;
  height: 1rem;
  width: 1rem;
  background-color: #FFF;
}

.overflow {
  display: flex;
  justify-content: space-between;
}

.overflow>div {
  width: 0;
  display: flex;
  justify-content: center;
}
<body>
  <div class="wrapper">
    <div class="fit_content_box">
      <p class="L">Lorem ipsum dolor sit e dolor sit e</p>

      <div class="R"></div>
    </div>

    <div class="overflow">
      <div>
        <p>0</p>
      </div>
      <div>
        <p>12</p>
      </div>
      <div>
        <p>24</p>
      </div>
    </div>
  </div>
</body>

To better illustrate the (1) here is another example with overflowing margin that you can hardly notice:

.container {
  width:200px;
  margin:auto;
  display:flex;
  justify-content:center;
}
.box {
  height:50px;
  width:100%;
  background:red;
}
<div class="container">
  <div class="box" style="margin:0 5966px">a_long_text_to_avoid_the_shrink</div>
</div>

<div class="container">
  <div class="box">a_long_text_to_avoid_the_shrink</div>
</div>

You can see that we have a long text forcing our element to not shrink (the min-width constraint), the element is taking full width and we are centring the content. This will make the margin overflowing like if there is no margin.

If you break one rule then you will see the effect of the margin.

Remove the long text:

.container {
  width:200px;
  margin:auto;
  display:flex;
  justify-content:center;
}
.box {
  width:100%;
  height:50px;
  background:red;
}
<div class="container">
  <div class="box" style="margin:0 5966px">a long text to avoid the shrink</div>
</div>

<div class="container">
  <div class="box">a long text to avoid the shrink</div>
</div>

Remove the centring:

.container {
  width:200px;
  margin:auto;
  display:flex;
}
.box {
  width:100%;
  height:50px;
  background:red;
}
<div class="container">
  <div class="box" style="margin:0 5966px">a_long_text_to_avoid_the_shrink</div>
</div>

<div class="container">
  <div class="box">a_long_text_to_avoid_the_shrink</div>
</div>

Make a different margin on each side

.container {
  width:200px;
  margin:auto;
  display:flex;
  justify-content:center;
}
.box {
  width:100%;
  height:50px;
  background:red;
}
<div class="container">
  <div class="box" style="margin:0 500px 0 400px">a_long_text_to_avoid_the_shrink</div>
</div>

<div class="container">
  <div class="box">a_long_text_to_avoid_the_shrink</div>
</div>

(2) The white-space is creating the min-width contraint preventing the element from shrinking.

Here is an exmaple to illustrate:

.body {
  display: flex;
  justify-content: center;
  margin: 10px 100px;
  border: 2px solid red;
}

.wrapper {
  border: 1px solid;
  margin: 0 20px;
}
.box {
 display:flex;
}
The below is a logical behavior where the text will wrap and the margin are respected
<div class="body">
  <div class="wrapper">
    <div class="box">
      <div>some long text here some long text here some long text here some long text here</div>
    </div>
  </div>
</div>
Let's add white-space:nowrap. We add a min-width contraint since we said to the text to never wrap thus our flex element will not shrink and overflow.
<div class="body">
  <div class="wrapper">
    <div class="box">
      <div style="white-space:nowrap">some long text here some long text here some long text here some long text here</div>
    </div>
  </div>
</div>
If we add width:100% we force its width to be the same as the container BUT the margin aren't included and are kept outside (the text will logically overflow)
<div class="body">
  <div class="wrapper" style="width:100%">
    <div class="box">
      <div style="white-space:nowrap">some long text here some long text here some long text here some long text here</div>
    </div>
  </div>
</div>
Now if we add min-width:0 we remove the constaint of minimum sizing and we can see the margin again even if we keep width:100% because the element will shrink by default
<div class="body">
  <div class="wrapper" style="width:100%;min-width:0">
    <div class="box">
      <div style="white-space:nowrap">some long text here some long text here some long text here some long text here</div>
    </div>
  </div>
</div>

The trick is that we are centring the element and applying the same margin on both side which will create the illusion of a collapsing margin but it's a simple overflow of the margin from both sides equally.

Let's change the margin slightly on one side to see a little offset to the other side:

.body {
  display: flex;
  justify-content: center;
  margin: 10px 100px;
  border: 2px solid red;
}

.wrapper {
  border: 1px solid;
  margin: 0 20px 0 40px;
}
.box {
 display:flex;
}
The below is a logical behavior where the text will wrap and the margin are respected
<div class="body">
  <div class="wrapper">
    <div class="box">
      <div>some long text here some long text here some long text here some long text here</div>
    </div>
  </div>
</div>
Let's add white-space:nowrap. We add a min-width contraint since we said to the text to never wrap thus our flex element will not shrink and overflow.
<div class="body">
  <div class="wrapper">
    <div class="box">
      <div style="white-space:nowrap">some long text here some long text here some long text here some long text here</div>
    </div>
  </div>
</div>
If we add width:100% we force its width to be the same as the container BUT the margin aren't included and are kept outside (the text will logically overflow)
<div class="body">
  <div class="wrapper" style="width:100%">
    <div class="box">
      <div style="white-space:nowrap">some long text here some long text here some long text here some long text here</div>
    </div>
  </div>
</div>
Now if we add min-width:0 we remove the constaint of minimum sizing and we can see the margin again even if we keep width:100% because the element will shrink by default
<div class="body">
  <div class="wrapper" style="width:100%;min-width:0">
    <div class="box">
      <div style="white-space:nowrap">some long text here some long text here some long text here some long text here</div>
    </div>
  </div>
</div>
Temani Afif
  • 245,468
  • 26
  • 309
  • 415
  • The reason I do `width: 100%` is so that the wrapper will occupy at least the max-width if space allows for it. If the contents of .L don't overflow, then the wrapper collapses in width. Here's an example pen: https://codepen.io/HybridCore/pen/rXxaby –  Jul 23 '19 at 01:48
  • Basically I want the wrapper to maintain max-width if the viewport allows for it, regardless of whether or not .L has enough text to push the wrapper to max-width. –  Jul 23 '19 at 01:55
  • 1
    @HybridCore it's easy, simply add `flex-grow: 1;` to it to allow it to grow, don't use `width:100%` – Temani Afif Jul 23 '19 at 08:09
  • A classic `min-width` default problem ([which I've written about so much!](https://stackoverflow.com/q/36247140/3597276)). I totally overlooked the flex container on the `body` element. Thank you for your time on this. Great answer. – Michael Benjamin Jul 28 '19 at 14:09
4

It appears that the source of the problem is white-space: nowrap, which is applied to the content element (.L) inside the first child item (.fit_content_box).

.L {
    border: solid 1px #FF0000;
    min-width: 0;
    flex: 1 0;
    overflow: hidden;
    white-space: nowrap;    <--- trouble maker
    text-overflow: ellipsis;
}

If you remove that line of code, your side margins on .wrapper work as expected.

So the key questions are:

  • Why does the white-space property on a grand-child (.L) collapse the side margins of the grand-parent (.wrapper)?
  • Why does the white-space property not collapse the side margins when they are applied to the parent (.fit_content_box)?
  • Why does the overflow property, when applied to the grand-parent (.wrapper), with a value other than visible, allow the margins to hold firm on the grand-child (.L)?

You wrote:

Applying the margins to the first child didn't make it collapse with the parent since it's in a new block formatting context.

Actually, this isn't an issue of conventional margin collapsing, because:

  1. We're talking about horizontal margins, and horizontal margins never collapse, and

  2. We're working inside a flex container, and margins inside a flex container never collapse.

So although a full understanding of the problem may lie in the block (or flex) formatting context, I'm not sure that's why margins on the parent don't collapse.

This is as far as I've gotten on this issue. I'll do more research when I have time. Or maybe somebody else can pick it up from here.

Michael Benjamin
  • 346,931
  • 104
  • 581
  • 701
  • I get that `overflow: hidden` clips the contents to the padding (not content, at least looking at MDN and the CSS Overflow Module Level 3 definition). What I don't get, however, is why the wrapper's margin is being ignored. I thought the two child elements are strictly in the wrapper's content box from a placement perspective and will resize to fit that content box. From how I'm imagining it, there shouldn't be any effect on the wrapper's margin. –  Jul 13 '19 at 19:44
  • 1
    Yeah, that's a good point. I've gone through your question again and revised my answer. – Michael Benjamin Jul 20 '19 at 01:29
  • 1
    Title changed to reflect your answer. I tried doing some more testing but got no results. Strangely it only seems to respect its predecessor elements' margins if it's the body element and nothing else (your original suggestion for what to apply the margin to). Not sure if this is an implementation bug, a bug within the spec, or intended behavior. –  Jul 22 '19 at 03:55
  • 1
    My original answer suggested *padding*, not *margin* on the `body` element. My bet is that it's intended behavior. Maybe a bug, but the behavior is consistent across major browsers (I tested Chrome, FF & Edge). – Michael Benjamin Jul 22 '19 at 04:13
  • 1
    added an answer – Temani Afif Jul 22 '19 at 09:46
  • 2
    Feel free to accept the answer of @TemaniAfif, if you wish. He solved the mystery. – Michael Benjamin Jul 25 '19 at 01:49