CSS Module 4 – The box model, sizing and overflow
Margin collapse and formatting contexts
When vertical margins collapse into one, how to predict the gap, how flow-root, flex and grid stop it, and how auto margins centre and push boxes.
What you will learn
- Predict when vertical margins collapse between siblings, between a parent and child, and through empty blocks
- Prevent unwanted collapse with display flow-root, padding, borders or a flex or grid parent
- Use auto margins to centre blocks and to push items apart
- Choose a spacing strategy in which parents set the space between components with gap
Before you start
What you’ll learn · Before you start
What you will learn
- Predict when vertical margins collapse between siblings, between a parent and child, and through empty blocks
- Prevent unwanted collapse with display flow-root, padding, borders or a flex or grid parent
- Use auto margins to centre blocks and to push items apart
- Choose a spacing strategy in which parents set the space between components with gap
Before you start
On this page
When the bottom margin of one block meets the top margin of the next, CSS does not add them: they collapse into one margin, as large as the larger of the two. A paragraph with a 30 px bottom margin followed by one with a 20 px top margin sits 30 px above it, not 50 px. Collapse happens only between vertical margins of block boxes in normal flow, and only when nothing separates the margins: no padding, no border, no line of text. A formatting context is the boundary that stops it, which is why the same margins add up inside a flex or grid container. Knowing the rule explains most spacing surprises, such as a heading whose margin pushes its whole card down instead of making space inside it.
This lesson measures twelve cases in Chromium 153, Firefox 155 and WebKit 26.6, which agreed on every one.
The rule: the largest positive margin plus the most negative one
The CSS 2 specification states the rule in a few lines, and its effect fits in one line of code: take the largest positive margin among those that meet, and add the most negative one. If all of them are negative, the most negative wins; if they are all positive, the largest does.
// Adjoining vertical margins combine into one (CSS 2, section 8.3.1): the largest positive margin plus the most
// negative one. Edit the lists and run it again to predict a gap before you open DevTools.
function collapse(margins) {
const largestPositive = Math.max(0, ...margins);
const mostNegative = Math.min(0, ...margins);
return largestPositive + mostNegative;
}
const cases = [
['30px below a block, 20px above the next', [30, 20]],
['an empty block between: 10, 20, 30, 10', [10, 20, 30, 10]],
['30px and -10px', [30, -10]],
['-10px and -25px', [-10, -25]],
['h2 24px, its section 16px, a -8px pull', [24, 16, -8]],
];
for (const [label, margins] of cases) {
console.log(`${label.padEnd(42)} -> ${collapse(margins)}px`);
}
// Margins that are not adjoining are simply added: side by side, between flex or grid items, or across padding.
console.log(`${'30px and 20px in a flex column'.padEnd(42)} -> ${30 + 20}px`); Output
30px below a block, 20px above the next -> 30px an empty block between: 10, 20, 30, 10 -> 30px 30px and -10px -> 20px -10px and -25px -> -25px h2 24px, its section 16px, a -8px pull -> 16px 30px and 20px in a flex column -> 50px
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node collapse.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
The rule exists for text. The HTML standard gives every paragraph margin-block: 1em, and because the margins
collapse, two paragraphs in a row are 1em apart, not 2em. Margins of the root element never collapse, and horizontal
margins never collapse at all.
Where margins meet
Margins are adjoining, and so collapse, in three situations, as long as no padding, border, line box or clearance lies between them and both boxes are in normal flow in the same block formatting context:
- the bottom margin of a block and the top margin of the next sibling;
- the top margin of a block and the top margin of its first child, or the bottom margin of a block whose height is
autoand the bottom margin of its last child; - the top and bottom margins of an empty block, which has no height, no padding, no border and no content: its two margins collapse with each other and with the margins around it.
Two questions decide whether two margins collapse
Text description of the diagram
The diagram is a column of steps that starts where two vertical margins meet: two siblings, a parent and its first or last child, or the top and bottom of an empty block.
- The first question asks whether both boxes are in normal flow, in the same block formatting context. If not, the margins do not collapse and add up. That is the case for flex and grid items, floats, inline-blocks and the children of a flow-root box.
- If they are, the second question asks whether padding, a border, a line of text or clearance separates the margins. If something does, they add up too.
- If nothing does, the margins collapse into one margin: the largest positive margin plus the most negative one.
The lab puts each case in a box of its own and measures the result with getBoundingClientRect(). For the six
parent cases it reports how far the child’s 24 px margin ends up above the parent and how far inside it:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Margin collapse lab</title>
<style>
body { margin: 0; }
/* Each case sits in its own bordered flow-root box, so no case can collapse with another. */
.case { display: flow-root; border: 1px solid #999; margin-block-end: 8px; }
.block { block-size: 24px; background: #dde; }
/* Blocks that follow each other: the first has a bottom margin, the second a top margin. */
#siblings .first { margin-block-end: 30px; }
#siblings .second { margin-block-start: 20px; }
#empty .first { margin-block-end: 10px; }
#empty .nothing { margin-block: 20px 30px; } /* an empty block: its two margins touch */
#empty .second { margin-block-start: 10px; }
#mixed .first { margin-block-end: 30px; }
#mixed .second { margin-block-start: -10px; }
#negative .first { margin-block-end: -10px; }
#negative .second { margin-block-start: -25px; }
#flex-column { display: flex; flex-direction: column; }
#flex-column .first { margin-block-end: 30px; }
#flex-column .second { margin-block-start: 20px; }
.side { display: inline-block; inline-size: 40px; block-size: 24px; background: #dde; }
#side-by-side .first { margin-inline-end: 30px; }
#side-by-side .second { margin-inline-start: 20px; }
/* A parent whose first child has a 24px top margin. */
.marker { block-size: 10px; background: #edd; }
.child { margin-block-start: 24px; block-size: 24px; background: #ded; }
#padded .parent { padding-block-start: 1px; }
#flow-root .parent { display: flow-root; }
#overflow-hidden .parent { overflow: hidden; }
#overflow-clip .parent { overflow: clip; }
#container .parent { container-type: inline-size; }
</style>
</head>
<body>
<div class="case" id="siblings"><div class="block first"></div><div class="block second"></div></div>
<div class="case" id="empty"><div class="block first"></div><div class="nothing"></div><div class="block second"></div></div>
<div class="case" id="mixed"><div class="block first"></div><div class="block second"></div></div>
<div class="case" id="negative"><div class="block first"></div><div class="block second"></div></div>
<div class="case" id="flex-column"><div class="block first"></div><div class="block second"></div></div>
<div class="case" id="side-by-side"><span class="side first"></span><span class="side second"></span></div>
<div class="case" id="plain"><div class="marker"></div><div class="parent"><div class="child"></div></div></div>
<div class="case" id="padded"><div class="marker"></div><div class="parent"><div class="child"></div></div></div>
<div class="case" id="flow-root"><div class="marker"></div><div class="parent"><div class="child"></div></div></div>
<div class="case" id="overflow-hidden"><div class="marker"></div><div class="parent"><div class="child"></div></div></div>
<div class="case" id="overflow-clip"><div class="marker"></div><div class="parent"><div class="child"></div></div></div>
<div class="case" id="container"><div class="marker"></div><div class="parent"><div class="child"></div></div></div>
<script>
const box = (selector) => document.querySelector(selector).getBoundingClientRect();
const px = (n) => Math.round(n * 100) / 100; // engines measure in fractions of a pixel
for (const id of ['siblings', 'empty', 'mixed', 'negative', 'flex-column']) {
console.log(`${id}: ${px(box(`#${id} .second`).top - box(`#${id} .first`).bottom)}px between the blocks`);
}
console.log(`side-by-side: ${px(box('#side-by-side .second').left - box('#side-by-side .first').right)}px between the boxes`);
// Where the child's 24px top margin ends up: above the parent, or inside it.
for (const id of ['plain', 'padded', 'flow-root', 'overflow-hidden', 'overflow-clip', 'container']) {
const above = px(box(`#${id} .parent`).top - box(`#${id} .marker`).bottom);
const inside = px(box(`#${id} .child`).top - box(`#${id} .parent`).top);
console.log(`${id}: ${above}px above the parent, ${inside}px inside it`);
}
</script>
</body>
</html> | Case | Margins | Result |
|---|---|---|
| siblings | 30 and 20 px | 30 px apart |
| an empty block between | 10, 20, 30, 10 px | 30 px apart |
| mixed signs | 30 and −10 px | 20 px apart |
| both negative | −10 and −25 px | −25 px: overlap |
| a flex column | 30 and 20 px | 50 px apart |
| side by side | 30 and 20 px | 50 px apart |
| parent, first child | 24 px on the child | 24 above, 0 inside |
| parent, 1 px padding | the same | 0 above, 25 inside |
parent, flow-root |
the same | 0 above, 24 inside |
parent, overflow: hidden |
the same | 0 above, 24 inside |
parent, overflow: clip |
the same | 24 above, 0 inside |
parent, container-type |
the same | 0 above, 24 inside |
The parent and first child row is the case people run into most. The child’s margin collapses with its parent’s top margin, the parent has none of its own, so the combined 24 px margin belongs to the parent: it appears above the parent and pushes it down, while the child sits flush with the parent’s top edge. On a page, it looks like a card or a section moving down for no reason, with the space outside its background instead of inside it.
What stops margins from collapsing
Anything that separates two margins keeps them apart. A single pixel of padding or border is enough, as the 1 px padding row shows, and so is a line of text between them. The other way is a box that starts a new formatting context: the layout inside it is independent of the layout around it, and margins never collapse across its edge. These boxes start one:
display: flow-rootstarts one with no side effects: it exists for this.- Flex and grid containers start one, and their items’ margins never collapse with each other either; the children are laid out as flex or grid items.
display: inline-blockand table cells start one; the box sits in a line of text, or in a table.- Floats and absolutely or fixed positioned boxes start one, and leave normal flow.
overflow: hidden,autoorscrollstart one; the box also clips and becomes a scroll container.overflow: clipdoes not. It clips, and nothing else.container-type: inline-sizeorsizestart one; the box’s width then no longer depends on its content.contain: layout,paint,contentorstrictstart one, as part of their containment.
Two of these catch people out. overflow: clip clips like hidden but deliberately does not start a formatting
context, so margins still collapse through it, as the lab measured. And container-type, which you add to use
container queries, starts one, so adding it to a wrapper can move the spacing of its first and last child before any
@container rule matches.
display: flow-root, the side-effect-free fix
A block formatting context is the region in which blocks stack one after another. The box that starts one
contains its own floats, grows to wrap them, keeps floats outside it from overlapping its content, and keeps its
children’s margins inside. display: flow-root makes an ordinary block do exactly that and nothing more. It is
Baseline widely available, and it replaces the older “clearfix” rules that added an invisible pseudo-element after
the floats.
For the card whose heading margin leaks out, there are two good fixes:
/* The problem: the heading's top margin collapses through the card and pushes the whole card down. */
.card h2 {
margin-block-start: 24px;
}
/* Fix 1: the card becomes a block formatting context, so the heading's margin stays inside it. */
.card {
display: flow-root;
}
/* Fix 2: no margin to leak: the card's padding makes the space, and its first child starts at the top. */
.card {
padding: 24px;
}
.card > :first-child {
margin-block-start: 0;
} Choose flow-root when the card’s spacing should come from its children’s margins. Choose padding when the card is
a component whose inner spacing it controls itself; then removing the first child’s top margin also keeps the space
the same whatever element comes first.
Auto margins: centring and pushing apart
A margin of auto takes up free space, and what counts as free space depends on the layout. The page below has a
600 px column in an 800 px window, a 200 px box in a 120 px tall block, a navigation bar and a flex container with one
item:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Auto margins in block and flex layout</title>
<style>
body { margin: 0; font: 16px/24px Arial, sans-serif; }
/* Block layout: auto margins on both sides share the free space, which centres the box. */
.page { max-inline-size: 600px; margin-inline: auto; }
/* In normal flow, auto top and bottom margins count as 0. */
.tall { block-size: 120px; }
.tall .note { inline-size: 200px; margin: auto; }
/* Flex layout: an auto margin takes all the free space on its side. */
nav { display: flex; gap: 8px; inline-size: 600px; }
nav a { padding: 8px; }
nav .account { margin-inline-start: auto; }
.hero { display: flex; block-size: 120px; }
.hero .note { margin: auto; }
</style>
</head>
<body>
<div class="page">A 600px column</div>
<div class="tall"><p class="note">margin: auto in a block</p></div>
<nav><a href="#">Home</a><a href="#">Shop</a><a class="account" href="#">Account</a></nav>
<div class="hero"><p class="note">margin: auto in a flex container</p></div>
<script>
const r = (selector) => document.querySelector(selector).getBoundingClientRect();
const px = (n) => Math.round(n * 100) / 100; // engines measure in fractions of a pixel
console.log(`column in an 800px window: starts at ${r('.page').left}px, ends ${800 - r('.page').right}px from the right`);
console.log(`block, margin: auto: the note starts ${r('.tall .note').top - r('.tall').top}px below the top of its 120px box`);
console.log(`nav: Account ends at ${px(r('nav .account').right)}px, the right end of the 600px nav`);
const hero = r('.hero'), note = r('.hero .note');
console.log(`flex, margin: auto: the note has ${note.top - hero.top}px above it and ${hero.bottom - note.bottom}px below it`);
</script>
</body>
</html> - In block layout,
margin-inline: autoon the 600 px column centred it, with 100 px on each side. - In block layout,
margin: autoon the 200 px box left it at the top: auto top and bottom margins count as 0. - In the flex row, an auto start margin on the last link moved it to the far end.
- In the flex container,
margin: autoon its only item centred it, with 48 px above and below.
In normal flow, margin-inline: auto shares the unused width equally between the two sides, so it centres any
block that has a width or a maximum width smaller than its container; without one, the block already fills the line
and there is nothing to share. Top and bottom auto margins count as zero there. In a flex container, auto
margins take all the positive free space on their side before justify-content runs, in both directions: one auto
margin pushes an item to the far end, such as an account link at the right of a navigation bar, and margin: auto on
a single item centres it horizontally and vertically.
A spacing strategy that avoids surprises
Collapse is predictable once you know the rule, but it is easier not to depend on it. A common pattern in design systems is that a component sets no margin on its outer edge, and the container that places components sets the space between them:
/* Components have no outer margin; the container that places them decides the space between them. */
.stack {
display: flex;
flex-direction: column;
gap: 16px; /* between items only: nothing before the first or after the last, and nothing collapses */
}
/* Text flow, where the elements are paragraphs and headings: space before every element but the first. */
.flow > * + * {
margin-block-start: 1em;
} gap works in flex and grid containers (and in multi-column layout). It adds space only between items, never before
the first or after the last one, and since the margins of flex and grid items never collapse, items without margins
of their own end up exactly 16 px apart with gap: 16px.
For running text, where paragraphs and headings follow each other in normal flow, the .flow > * + * rule gives
every element but the first a top margin, so only one margin ever sits between two blocks. The margin-trim
property, which removes children’s margins at the edges of their container, would help here too, but only Safari
supports it so far.
Practise: predict the gap
The exercise turns the rules of this lesson into two functions, and its tests include the cases of the lab.
Exercise · Medium · JavaScript
Predict where a heading's top margin ends up
A card whose heading has a top margin can show that space inside the card, or above it, where it pushes the whole card down. Write two functions in gaps.mjs that predict it, and export them. All lengths are in px.
The first function, collapse(margins), returns the single margin that a list of adjoining vertical margins becomes: the largest positive margin plus the most negative one. collapse([30, -10]) is 20, collapse([-10, -25]) is -25, and an empty list gives 0.
The second, childStart({ parent, childMargin }), describes a parent block and the top margin of its first child, and returns { above, inside }: how far the parent's border box starts below the block before it (which has no margin of its own), and how far below the parent's border-box top the child's border box starts. parent may leave out any of these, which then have the values in brackets: margin (its own top margin, 0), paddingTop (0), borderTop (0), display ('block'), overflow ('visible') and containerType ('normal').
- The two margins collapse when nothing separates them, that is with no top padding and no top border, and when the parent does not start a new formatting context. Then
aboveiscollapse([parent.margin, childMargin])andinsideis0. - The parent starts a new formatting context when its
displayis'flow-root','flex','grid'or'inline-block', when itsoverflowis'hidden','auto'or'scroll'(not'clip'), or when itscontainerTypeis'inline-size'or'size'. - Otherwise
aboveis the parent's own margin andinsideis the border, the padding and the child's margin added.
The sample tests import both functions from gaps.mjs and run in your browser; several of them are the cases of the lesson's lab, measured in three browser engines.
Starter code · gaps.mjs
/** The one margin that adjoining vertical margins collapse into (px). */
export function collapse(margins) {
// Replace this line with your code.
return 0;
}
/** Where a first child's top margin ends up: { above, inside } (px). */
export function childStart({ parent, childMargin }) {
// Replace this line with your code.
return { above: 0, inside: childMargin };
} The sample tests · gaps.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { collapse, childStart } from './gaps.mjs';
test('the largest positive margin wins', () => {
assert.equal(collapse([30, 20]), 30);
assert.equal(collapse([10, 20, 30, 10]), 30);
});
test('negative margins are taken off, and all-negative lists keep the most negative', () => {
assert.equal(collapse([30, -10]), 20);
assert.equal(collapse([24, 16, -8]), 16);
assert.equal(collapse([-10, -25]), -25);
assert.equal(collapse([-5]), -5);
});
test('no margins, or only zeros, make 0', () => {
assert.equal(collapse([]), 0);
assert.equal(collapse([0, 0]), 0);
});
test('a plain parent lets the child margin out (the lab: 24px above, 0 inside)', () => {
assert.deepEqual(childStart({ parent: {}, childMargin: 24 }), { above: 24, inside: 0 });
assert.deepEqual(childStart({ parent: { display: 'block', overflow: 'visible' }, childMargin: 24 }), { above: 24, inside: 0 });
});
test("the parent's own margin collapses with the child's", () => {
assert.deepEqual(childStart({ parent: { margin: 16 }, childMargin: 24 }), { above: 24, inside: 0 });
assert.deepEqual(childStart({ parent: { margin: 32 }, childMargin: 24 }), { above: 32, inside: 0 });
assert.deepEqual(childStart({ parent: { margin: 16 }, childMargin: -8 }), { above: 8, inside: 0 });
});
test('padding or a border keeps the margin inside (the lab: 1px of padding gives 25px)', () => {
assert.deepEqual(childStart({ parent: { paddingTop: 1 }, childMargin: 24 }), { above: 0, inside: 25 });
assert.deepEqual(childStart({ parent: { borderTop: 2 }, childMargin: 24 }), { above: 0, inside: 26 });
assert.deepEqual(childStart({ parent: { margin: 16, paddingTop: 8, borderTop: 1 }, childMargin: 24 }), { above: 16, inside: 33 });
});
test('flow-root, flex, grid and inline-block start a new formatting context', () => {
for (const display of ['flow-root', 'flex', 'grid', 'inline-block']) {
assert.deepEqual(childStart({ parent: { display }, childMargin: 24 }), { above: 0, inside: 24 }, display);
}
assert.deepEqual(childStart({ parent: { display: 'flow-root', margin: 10 }, childMargin: 24 }), { above: 10, inside: 24 });
});
test('overflow hidden, auto and scroll do too, but clip does not', () => {
for (const overflow of ['hidden', 'auto', 'scroll']) {
assert.deepEqual(childStart({ parent: { overflow }, childMargin: 24 }), { above: 0, inside: 24 }, overflow);
}
assert.deepEqual(childStart({ parent: { overflow: 'clip' }, childMargin: 24 }), { above: 24, inside: 0 });
});
test('a size container is a new formatting context', () => {
assert.deepEqual(childStart({ parent: { containerType: 'inline-size' }, childMargin: 24 }), { above: 0, inside: 24 });
assert.deepEqual(childStart({ parent: { containerType: 'size' }, childMargin: 24 }), { above: 0, inside: 24 });
assert.deepEqual(childStart({ parent: { containerType: 'normal' }, childMargin: 24 }), { above: 24, inside: 0 });
}); A hint
For collapse, keep two running values that start at 0: the largest margin and the smallest one. The answer is their sum, so [30, -10] gives 30 + (−10). For childStart, first decide with one boolean whether the margins can touch: no top padding, no top border, and none of the listed values that start a formatting context.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Interview questions
Fresher · What is margin collapsing, and does it happen to horizontal margins?
Margin collapsing is CSS combining adjoining vertical margins of block boxes into one margin instead of adding them.
The result is the largest positive margin plus the most negative one, so 30 px above 20 px gives 30 px, and 30 px
with −10 px gives 20 px. It happens between a block and its next sibling, between a parent and its first or last
child when no padding or border separates them, and through an empty block. It only happens in normal flow, in the
same block formatting context: margins of flex and grid items, floats, absolutely positioned boxes and inline-blocks
never collapse, and neither do margins across the edge of a flow-root or overflow: hidden box. Horizontal margins
never collapse; two inline-blocks with 30 px and 20 px margins between them sit 50 px apart.
Key takeaways
- Adjoining vertical margins collapse into one: the largest positive margin plus the most negative one. Horizontal margins never collapse.
- They meet between siblings, between a parent and its first or last child, and through an empty block, as long as no padding, border or line of text separates them.
- A child’s top margin that collapses with its parent’s appears above the parent: give the parent padding, or make it
display: flow-root. - Flex and grid containers, floats, inline-blocks,
overflowother thanvisibleorclip, and size containers start a new formatting context, and margins do not collapse across it.overflow: clipdoes not. margin-inline: autocentres a block that has a width; in flex layout, auto margins take all the free space and can push an item to the end or centre it both ways.- Let parents set the space between components with
gap, and keep components free of outer margins.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- CSS 2: collapsing margins (W3C CSS Working Group)
- CSS 2: block formatting contexts (W3C CSS Working Group)
- CSS 2: block-level, non-replaced elements in normal flow (auto margins) (W3C CSS Working Group)
- CSS Display Module Level 3: independent formatting contexts and flow-root (W3C CSS Working Group)
- CSS Overflow Module Level 3: the clip value (W3C CSS Working Group)
- CSS Conditional Rules Module Level 5: the container-type property (W3C CSS Working Group)
- CSS Containment Module Level 3: inline-size containment (W3C CSS Working Group)
- CSS Containment Module Level 2: layout and paint containment (W3C CSS Working Group)
- CSS Flexible Box Layout Module Level 1: flex item margins and auto margins (W3C CSS Working Group)
- CSS Box Alignment Module Level 3: gaps between boxes (W3C CSS Working Group)
- CSS Box Model Module Level 4: the margin-trim property (W3C CSS Working Group)
- HTML Standard: rendering, the default margins of paragraphs and the body (WHATWG)
- Mastering margin collapsing (MDN Web Docs) (Mozilla)
- Block formatting context (MDN Web Docs) (Mozilla)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress