先记住这个答案
外边距合并是常规流里块级元素的规则:上一个块的 margin-bottom 和下一个块的 margin-top 挨在一起时,不会相加,而是合并成一个。两个都是正数就取较大的,所以各 20px 最后只有 20px。这样设计是为了排版:段落都写 margin: 1em 0,段与段之间就是 1em,不会变成 2em。想避免合并,最简单的是让父容器变成 Flex 或 Grid,或者团队约定只用单方向的外边距。
- 相邻块的上下外边距合并,都为正时取较大值
- 一正一负时相加,都为负时取绝对值最大的
- 左右外边距永远不合并
- Flex、Grid 子项之间不会合并
正负外边距怎么算
合并后的值不是简单地取最大,要看正负。
<style>
.a { margin-bottom: 30px; }
.b { margin-top: 20px; } /* 都为正:取大的,间距 30px */
.c { margin-bottom: 30px; }
.d { margin-top: -10px; } /* 一正一负:相加,间距 20px */
.e { margin-bottom: -30px; }
.f { margin-top: -10px; } /* 都为负:取绝对值大的,间距 -30px,两块重叠 */
</style>规则可以记成一句:最大的正值加上最小的负值。只有正数时负值部分是 0,只有负数时正值部分是 0。三个以上的外边距连在一起合并时也是这么算,比如父元素的 margin-top、它第一个子元素的 margin-top、上一个兄弟的 margin-bottom 可能一起合并。
什么情况下不会合并
合并只发生在常规流的块级盒子之间,而且两个外边距要真的挨着。下面这些情况都不会合并。
父容器是 Flex 或 Grid 时,子元素之间的外边距各算各的,20px 加 20px 就是 40px。现在用 gap 控制间距的布局越来越多,基本不会碰到合并问题。
元素是浮动、绝对定位或 inline-block 时,它脱离了常规流里块与块的排列,外边距不和别人合并。
左右外边距在任何情况下都不合并。所以这个问题只和垂直方向有关,严格说是和块轴方向有关,竖排文字时合并的是左右外边距。
- BFC 为什么挡不住兄弟合并
- 很多资料说「触发 BFC 就能阻止外边距合并」,这句话只对了一半。BFC 是一个独立的布局区域,它隔开的是自己和子元素之间的外边距,典型场景是父元素的
margin-top和第一个子元素的margin-top合并。但 BFC 元素本身仍然在外层的常规流里,它的外边距照样和兄弟合并。想隔开两个兄弟,要把其中一个包进一个新的 BFC 容器里,或者直接用 Flex 布局。
利用和规避的实际做法
利用合并的典型例子是文章排版。标题、段落、列表各自写上下外边距,不管谁挨着谁,间距都是两者中较大的那个,不会出现标题下面空一大块的情况。
规避合并最省心的做法,是团队约定只用 margin-bottom,不写 margin-top。外边距只朝一个方向,就不存在两个挨在一起的情况。组件库做列表、表单项时也常用这个约定,或者直接交给父容器的 gap。
容易答错的地方
- 以为合并是取两个值的和
- 合并的意思是只留一个,不是叠加。都为正时结果是较大值,只有一正一负时才是相加。
- 以为设了
display: flow-root兄弟就不合并 flow-root让元素自己成为 BFC,隔开的是它和子元素,不是它和兄弟。兄弟之间照样合并。- 以为空元素不影响间距
- 一个没有内容、边框、内边距和高度的空块,它自己的上下外边距会先合并,再和前后兄弟的外边距一起合并。页面里留着一个空
div带着外边距,间距就可能莫名变大。
面试官还会怎么问?
父子之间的外边距合并怎么解决?
子元素的 margin-top 会穿出父元素,和父元素的外边距合并,表现为父元素整体往下移。给父元素加 padding-top 或 border-top 隔开,或者设 display: flow-root 建立 BFC 都能解决。
为什么 Flex 布局里不会合并?
Flex 项目按 Flex 布局算法排列,不是常规流里的块级盒子。规范只给常规流的块定义了外边距合并,Flex 和 Grid 都没有这条规则。
margin-top 和 margin-bottom 选哪个统一?
一般统一用 margin-bottom,因为文档是从上往下流的,每个元素负责和下一个元素拉开距离,最后一个元素用 :last-child 去掉即可。用 margin-top 也行,关键是全项目统一一个方向。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。