Note: This article assumes basic familiarity with CSS, particularly the box model and shorthand rules
Introduction
I ran across a quirk of CSS recently, related to the box model, that struck me as counter-intuitive behavior. To be honest, it's an issue I've repeatedly run into over the years that I've always bumbled my way around, and never fully understood.
Consider this simple example of a <div>
element with a child <p>
element that has top and bottom padding.
<style> p { padding: 10px 0; } </style> <div> <p>Some example text.</p> </div>
A full demo is below. Be sure to click the "HTML" button on each demo to view the full source, noting that the CSS is in a <style>
tag. The div
has an orange background color so its size can easily be differentiated from the bluish background of the body, and the p
has top and bottom padding. To clarify where exactly the spacing is coming from, the padding and margin of the body
and div
are explicitly zeroed out.
<!DOCTYPE html> <html lang="en-US"> <head> <style> body { background-color: #3a65aa; padding: 0; margin: 0; } div { margin: 0; padding: 0; background-color: #ffdaad; } p { margin: 0; padding: 10px 0; } </style> </head> <body> <div> <p>Some example text.</p> </div> </body> </html>
As expected, the content area of the div
expands to fit the paragraph's content and padding.
One might assume, as I did, that if the padding
attribute of the p
was changed to margin
, the end result on the page would look exactly the same, i.e. the content area of the div
would expand as its edges are "pushed" by the margin of the paragraph. However, this is not the case.
<!DOCTYPE html> <html lang="en-US"> <head> <style> body { background-color: #3a65aa; padding: 0; margin: 0; } div { margin: 0; padding: 0; background-color: #ffdaad; } p { margin: 10px 0; } </style> </head> <body> <div> <p>Some example text.</p> </div> </body> </html>
As seen in the example above, when the padding on the paragraph is changed instead to margin, the content area of the div
is wrapped tightly around the paragraph text rather than expanding. The margin now separates the edge of the div
from the top edge of the body
.
It now appears as though the margin applies to the div
rather than to the paragraph. This is the behavior that was unexpected for me. My (faulty) assumption was that given two block elements — a parent and child — the "box" of the child would always be fully contained inside the box of its parent. In this specific example, that the margins of the paragraph had an effect outside its parent element seemed almost like a bug (it's not, of course).
Collapsing Margins
The behavior seen in the second example above is an effect of something built-in to CSS called margin collapsing. Trusty friend MDN provides a succinct, though qualified, definition on its Mastering Margin Collapsing page:
The top and bottom margins of blocks are sometimes combined (collapsed) into a single margin whose size is the largest of the individual margins (or just one of them, if they are equal), a behavior known as margin collapsing.
I say qualified because the exceptions and caveats implied by the word "sometimes" in that definition are what can make the topic of margin collapsing frustrating. There are a lot of them. The current official definition of margin collapsing from W3C is comprised of two sets of rules, seven and nine bullet points each, some with nested bullets beneath. Excluding examples, it's roughly 850 words (that's 350 more than this article to this point). In short, determining if two particular elements' margins will collapse can be complicated.
For the sake of brevity, this article is limited to talking about block elements that are in the document flow (i.e. position: static
), and not floated.
...Why?
One might wonder why margin collapsing exists in the first place. Ultimately, its goal is to help the CSS writer create consistent, nicely-formatted spacing without writing lots of specific rules. Often, its effects are unnoticed, yet welcome.
Consider the following example:
<!DOCTYPE html> <html lang="en-US"> <head> <style> body, h2 { padding: 0; margin: 0; } section { padding: 5px; background-color: #ffdaad; } h2 { margin-bottom: 20px; } p { margin: 10px 0; } </style> </head> <body> <section> <h2>A Heading</h2> <p>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.</p> <p>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.</p> <p>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.</p> </section> </body> </html>
The p { margin: 10px 0; }
and h2 { margin-bottom: 20px; }
rules are a concise, intuitive way of fulfilling a hypothetical designer's request to "put 10 pixels of space between each paragraph, and 20 pixels between a heading and a paragraph".
But, margin-bottom
and margin-top
are technically both set to 10px
for each paragraph. Does this mean the paragraphs are ultimately 20 pixels apart (i.e. 10 from the bottom margin of one paragrph, 10 from the top margin of the next)? No! Each paragraph is only 10 pixels apart, thanks to the magic of collapsing margins.
Vertical margins collapsing allows consistent spacing to be achievable with considerably fewer rules. To illustrate this, consider how much more complicated it would be to implement the same simple page above if each margin-X
rule was taken literally. Especially if trying to write for general cases. Here's one way to implement it:
h2 { margin-bottom: 20px; } h2 + p { margin: 0 0 5px 0; } p + p { margin: 5px 0 5px 0; } p:last-of-type { margin: 5px 0 10px 0; }
Without margin collapse, separate rules are needed for paragraphs immediately following headings, paragraphs situated between other paragraphs, and the final paragraphs in sequences. It probably goes without saying that having to style entire sites this way would be a massive pain.
The rendered result, seen below, is in fact equivalent to the earlier, two-rule version which takes advantage of collapsing margins. But the CSS itself is far less clean and future-proof (For example, what happens when a <p>
follows something other than an <h2>
?).
<!DOCTYPE html> <html lang="en-US"> <head> <style> body, h2 { padding: 0; margin: 0; } section { float: left; /* This rule is here to disable margin collapsing */ padding: 5px; background-color: #ffdaad; } p { float: left; /* This rule is here to disable margin collapsing */ } h2 { margin-bottom: 20px; } h2 + p { margin: 0 0 5px 0; } p + p { margin: 5px 0 5px 0; } p:last-of-type { margin: 5px 0 10px 0; } </style> </head> <body> <section> <h2>A Heading</h2> <p>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.</p> <p>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.</p> <p>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.</p> </section> </body> </html>
What exactly is a margin, anyway?
It turns out I've been thinking about margins incorrectly for a long time. A week ago I might have defined a top margin as it relates to the box model by saying it's "the amount of space between the top of one element's border and the bottom margin of the element above it." In fact, this is not necessarily true. It's far more accurate to say a top margin is the minimum amount of space that should exist between one element and what's above it. If the element above demands more space in its bottom margin, that becomes the new minimum amount of space between the elements, and is what is rendered (in situations where the margins collapse, anyway).
Into the Spec
Perturbed by a CSS term of which I was previously blissfully unaware, I dug into the spec to see when this new-fangled concept first appeared. I'm rather embarassed to say margin collapsing dates back to CSS1, though it didn't get a formal section of the spec until CSS2. CSS 2.1 greatly expanded and clarified the earlier section. The CSS3 box model document yet again altered the wording of the section to be more mathematical, though changed nothing substantive as far as I'm aware.
Back to the Original Example
At this point the careful reader might be thinking: "Margin collapse is great for sibling elements, but what about the parent/child relationship of the original example?" Going back to the p
inside a div
example, why does the paragraph's margin seem to be "transferred" to the div
?
Buried in the spec are two points which answer that question. The first:
The top margin of an in-flow block element collapses with its first in-flow block-level child's top margin if the element has no top border, no top padding, and the child has no clearance.
Applied to the original example, the div
— an in-flow block element with an in-flow block-level child (the p
) — has no border or padding. So, it's margin collapses with that of the nested paragraph.
And the second:
If the element's margins are collapsed with its parent's top margin, the top border edge of the box is defined to be the same as the parent's.
This is what causes the "transfer". Recall that margins are outside the border. If the paragraph and div
share the same border edge, which they must because the paragraph's top margin is collapsed with its parent per the previous point, then the paragraph's top margin effectively becomes the div's top margin, which must go beyond its border by definition.
Put another way, when margins collapse in a parent/child relationship, there is no longer one "box" nested inside another box. Instead there is effectively a single shared top edge of a box being shared by both elements.