三个段落各写 margin: 20px,间距是 20 不是 40 —— 外边距塌陷

一段很常见的代码:

p { margin: 20px 0; }

三个段落连着排,期望段间距 40px(上下的 20 各出一份),实际量出来 20px。这不是浏览器算错,是 CSS 里叫 margin collapsing(外边距塌陷/合并)的规则:相邻的垂直外边距会合并成一个,取其中较大的那个,不是相加。

注意是”取较大的”,不是”取第一个”也不是”取平均值”。左边 30px、右边 20px,合并结果 30px;左边 -10px、右边 20px,结果是 10px(正负相消的算法不一样,这里不展开)。

四种会发生的情况

一、相邻兄弟。 就是上面那个例子。注意只有垂直方向会塌陷,左右不会。

二、父与子。 这条最反直觉。父元素没有 border-top、没有 padding-top、没有内容隔开时,第一个子元素的 margin-top 会”穿透”到父元素外面,把整个父元素往下推。

两分钟能自己验完:

<div style="background:#e5e7eb">
  <p style="margin:20px 0;background:#cbd5e1">A</p>
</div>

如果没塌陷,灰色父块的顶部应该紧贴 A 那一行文字的上方。实际看到的是:父块顶部和 A 之间有 20px 的缝隙,而那 20px 里露出来的是页面的背景色,不是父块的灰。子元素的 margin 跑到父元素外面去了。

三、空元素自塌陷。 一个高度为 0、没有 border/padding 的元素,自己的 margin-top 和 margin-bottom 会合并成一个。

<div style="margin:20px 0"></div>

它占据的垂直空间不是 40px,是 20px。

四、连续塌陷。 A 和 B 塌陷、B 和 C 塌陷,三者的结果可能合并成一个值。所以嵌套多层之后,间距会变得完全无法预测——这也是”我明明加了 margin 却没变化”的常见来源。

为什么 flex 里不塌陷

把父元素改成 display: flex; flex-direction: column,间距立刻正常了。很多人以为这是”因为 flex 建立了 BFC”——BFC 确实能阻止塌陷,但这不是这里的原因。

真正的原因更直接:margin collapsing 只发生在参与同一个块格式化上下文的块级盒之间。flex item 和 grid item 根本不参与外边距塌陷,规范里写得很明确。所以给容器加 display: flex 是从根上取消了这条规则,不是”绕过去”。

四种解法,按我的使用频率排

/* 1. 父元素建立新的 BFC,最干净 */
.parent { display: flow-root; }

/* 2. 父元素加 padding 或 border 隔开 —— 会改变视觉尺寸,慎用 */
.parent { padding-top: 1px; }

/* 3. 用 flex/grid 的 gap 取代 margin,现代项目首选 */
.parent { display: flex; flex-direction: column; gap: 20px; }
.child  { margin: 0; }

/* 4. 单向 margin 约定:只写下边距,永不用 margin-top */
p + p { margin-top: 20px; }

第一种我原来一直写 overflow: hidden,后来改成 display: flow-root——两者的 BFC 效果一样,但 flow-root 不会顺手把溢出内容裁掉,语义上也正是”我要一个独立的格式化上下文”。

一个常被混进来的东西

box-sizing: border-box 跟塌陷没有关系。它只管一件事:width 这个值里要不要包含 padding 和 border。塌陷管的是两个盒子之间垂直间距怎么算。这两个问题经常在”我的布局为什么不对”里一起出现,但改动其中一个不会影响另一个——先各自测一遍,别一起改。

← 返回技术笔记