Sean the Bean Sean the Bean - 5 months ago 28
CSS Question

CSS Right Margin Does Not Work Inside a Div With Overflow Scroll

I am trying to make two divs, one inside the other. The inner div is larger than the outer div, the outer div has

overflow:scroll
, and the inner div has
margin:25px
. So I do this:

#outer {
width: 200px;
height: 100px;
overflow: scroll;
}
#inner {
width: 400px;
height: 200px;
margin: 25px;
}


...

<div id="outer">
<div id="inner">

</div>
</div>


Instead of the inner div having a margin of 25px all the way around as expected, there is a 25px margin on THREE sides, but on the right side there is none. This is extremely counter-intuitive in my opinion.

If I add a middle div with a width large enough width to contain the inner div + 50px, we can make it look right, but that seems like a hacky workaround.

See my example on JSFiddle: http://jsfiddle.net/d3Nhu/16/

This happens the same way in every major browser. Is there any good reason for this behavior? Is this correct behavior according to the CSS specification?

NOTE: As you'd expect in this example, it makes no difference if you use
overflow:auto
instead of
overflow:scroll
.

EDIT: Please note that I'm not looking for a workaround for this behavior. (I already found one.) I'm looking for any insight as to the reason for this behavior, especially if it is documented in the CSS specification anywhere.

Answer

TL;DR:

Margins are for moving an element in from the wrapper, not expanding the wrapper outwards.

The long explanation:

This behavior is consistent with specifying a width in addition to a horizontal margin anywhere in the document. To break it down, consider the following snippet, where I specificity a wrapper without an overflow property, and the margin does not expand the wrapper element.

body {
    padding: 20px;
}
.outer {
    width: 400px;
    border: 1px solid black;
}
.inner {
    width: 400px;
    height: 40px;
    margin: 0 20px;
    background: grey;
}
<div class="outer">
    <div class="inner">
        
    </div>
</div>

As you can see, the margin did not cause the wrapper to expand in size, the element just continued to overflow. This behavior is documented under Visual formatting model details of the is documented in the CSS 2.1 specification.

Excerpt from the "Block-level, non-replaced elements in normal flow" section of "Visual formatting model details":

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 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.

That excerpt is quite dense, so to simply, let's ignore the width of border and padding, both of which are 0, leaving us with width, margin-left, and margin-right.

Now since the you have a fixed width and values for margin-left and margin-right, the values are "over-constrained". Now in our example, since the direction is ltr by default, the margin-right is forced to compensate.

To see the effects of the direction, let's try adding a dir="rtl" attribute to the wrapper element.

body {
    padding: 20px;
}
.outer {
    width: 400px;
    border: 1px solid black;
}
.inner {
    width: 400px;
    height: 40px;
    margin: 0 20px;
    background: grey;
}
<div class="outer" dir="rtl">
    <div class="inner">
        
    </div>
</div>

Now the element is overflowing to the left. Let's see if this dir="rtl" attribute has the same effect on your overflow: scroll example.

#outer {
    border: 1px solid #00F;
    width: 200px;
    height: 100px;
    overflow: scroll;
}
#inner {
    border: 1px solid #F0F;
    margin: 25px;
    width: 400px;
    height: 200px;
}
<div id="outer" dir="rtl">
    <div id="inner">
    
    </div>
</div>

Yep, it does. The margin is now missing on the left, rather than the right.

But why doesn't overflow: scroll include the margins?

Mainly because the specification does not say it should. Let's take a look at the CSS 2 specification for the overflow property.

Excerpt from the "Overflow and clipping" section of "Visual effects":

Whenever overflow occurs, the 'overflow' property specifies whether a box is clipped to its padding edge, and if so, whether a scrolling mechanism is provided to access any clipped out content.

See how it specifically says "clipped out content". For an explanation of "content", lets refer to the following graphic from the CSS 2 specification.

Graphic from the "Box dimensions" section of the "Box model":

box model

As we can see, the margin is separate from the content. However, at this point it's worth noting that padding and borders are included in the scrolling area, so when the the spec says "content", it is likely referring to border-box, or at least, that seems to be how it was interpreted.

Why does display: inline-block work?

Basically, margins behave differently on inline-block elements, because they are content level rather than block level, and they do not have a concept of being "over-constrained".

Comments