先记住这个答案
在普通文档流里,如果父元素的顶部没有 padding、border,也不是 BFC,那它的 margin-top 和第一个子元素的 margin-top 就是相邻的,会合并成一个外边距,取两者中较大的值。这个合并后的外边距显示在父元素的外面,于是看起来是子元素的外边距把父元素整体往下推,父元素的背景顶部和子元素贴在一起。解决办法是在两个外边距之间放点东西隔开,或者让父元素建立 BFC,推荐 display: flow-root。
- 父子上外边距可能合并
- 合并后取两者较大值,并显示在父元素外面
- 首选修法是父元素
display: flow-root - flex 和 grid 容器里子项的外边距不会和容器合并
复现一下:子元素的外边距跑到了外面
一个有背景色的卡片,里面第一个元素是标题,给标题加 margin-top 想让它离卡片顶部远一点。
<style>
.card { background: #eef; }
.card h2 { margin-top: 24px; }
</style>
<div class="card">
<h2>标题</h2>
<p>正文</p>
</div>
<!-- 预期:卡片顶部和标题之间有 24px 的浅蓝色 -->
<!-- 实际:标题紧贴卡片顶部,24px 空白出现在卡片上方 -->.card 顶部没有 padding 和 border,自己的 margin-top 是 0,和 h2 的 24px 合并后是 24px。这个合并后的外边距属于整个组合,渲染在 .card 外面,所以空白在卡片背景上方,而不是卡片里面。
h2 本身就有浏览器默认的上外边距,所以哪怕不写这行 CSS,没清过默认样式的项目也会遇到卡片顶部莫名多出一截空白。
合并的条件和取值
外边距合并只发生在普通流里的块级盒子之间,而且两个外边距之间必须是「挨着」的。父子之间,下面任何一样东西都能把它们隔开:父元素的 padding-top、border-top、父元素里排在子元素前面的文字或行内元素、清除浮动产生的间隙。
- 合并后到底是多少
- 两个都是正数时取较大的,24px 和 10px 合并是 24px,不是 34px。一正一负时相加,24px 和 -10px 合并是 14px。两个都是负数时取绝对值大的那个。这套规则对兄弟元素之间的合并也一样。
父元素是 BFC(块级格式化上下文,一个独立的布局区域,里面的外边距不会和外面的合并)时,也不会合并。能让父元素变成 BFC 的有
display: flow-root、overflow取hidden或auto、浮动、绝对定位、display: inline-block等。
几种修法和各自的代价
修法都不难,区别在于有没有副作用。
- 按推荐程度排
- 第一选择是父元素加
display: flow-root。它唯一的作用就是建立 BFC,没有别的副作用,专门为这类场景设计。 第二是给父元素加padding-top,把原来写在子元素上的间距挪到父元素上。卡片这类组件本来就该用内边距控制内容和边缘的距离,这个改法语义上最合理。 第三是border-top: 1px solid transparent,能用,但多出 1px 高度,属于绕路。overflow: hidden也能修,但它会裁掉阴影、下拉菜单这类溢出的内容,还会让父元素成为滚动容器,影响里面的position: sticky。以前常用它,现在有flow-root就没必要了。 另外,把父元素改成 flex 或 grid 容器,子项的外边距不会和容器合并。很多组件本来就是 flex 布局,所以会觉得「这个问题怎么消失了」。
容易答错的地方
- 以为合并后是两个外边距相加
- 正数之间取最大值,不是求和。父元素
margin-top: 10px、子元素margin-top: 24px,最终父元素上方只有 24px。 - 以为
overflow: clip也能修 overflow: clip只裁剪,不让元素成为滚动容器,也不建立 BFC,外边距照样合并。能修的是hidden、auto、scroll。- 以为只有顶部会合并
- 底部同样会合并。父元素高度是
auto、底部没有padding和border时,最后一个子元素的margin-bottom会跑到父元素下面。表现是父元素的背景没包住那段空白,下一个兄弟元素被推远。
面试官还会怎么问?
水平方向的外边距会合并吗?
不会。外边距合并只发生在块方向,也就是横排文字下的上下方向。左右外边距永远是相加的。
空元素自己的上下外边距会合并吗?
会。一个没有内容、没有高度、没有 padding 和 border 的空块元素,它的 margin-top 和 margin-bottom 会合并成一个,并且还可能继续和相邻元素的外边距合并。列表里偶尔出现一个空的 <p>,就会造成这种意料之外的间距。
实际项目里怎么避免反复踩这个坑?
组件内部统一用 padding 控制内容和边框的距离,子元素之间的间距用 flex 或 grid 的 gap,只在一个方向上给外边距。这样父子之间根本没有两个外边距相遇的机会。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。