深色屏幕上高亮的代码
前端 4 分钟

CSS 外边距塌陷:一个空段落引发的排版 bug

署名和正文之间莫名空出 158 像素。量一量、拆一拆,根因是空段落让标题的上边距塌了出去。

前几天改博客主题,遇到一个很小的视觉问题:文章页里,作者署名到正文第一行之间,空了一大块。

一开始以为是 padding 设大了,量了一下——158 像素。这个数字说明不只是 padding 的问题,因为那块区域的内边距加起来只有 100 像素。

一、把 158 像素拆开

打开开发者工具,量了几个盒子的位置,拆出来是这样:

署名底部      → 标题区底部    44px   (标题区的 padding-bottom)
标题区底部    → 正文区顶部     0px
正文区顶部    → 正文内容顶部  56px   (正文区的 padding-top)
正文内容顶部  → 第一个字      57.6px  ← 这 57.6px 是哪来的?

前两项正常,最后一项就很奇怪了:正文容器的 padding 和 margin 都是 0,第一个元素却比容器顶部低了 57.6 像素。

二、根因:外边距塌陷

看正文的 DOM 结构,发现是这样:

<div class="gh-content">
  <p></p>                            ← 空段落
  <h2 id="abstract">Abstract</h2>   ← 第一个有内容的元素
  <p>This study investigates...</p>
</div>

第一个元素是一个空段落。而后面那个 h2 的样式里有:

.gh-content h2 { margin-top: 2.4em; }   /* 24px × 2.4 = 57.6px */

问题就出在这。CSS 里有一条规则叫外边距塌陷(margin collapsing):相邻的两个外边距会合并成一个,取较大的那个。

而一个高度为 0 的空元素,它的上下外边距还会穿透它继续往上合并。

于是 h2 的 57.6 像素上边距,穿过那个空段落,又穿过没有 padding、没有 border 的 .gh-content,最后变成了整个正文容器上方的间距。内容整体被往下顶了 57.6 像素。

44 + 56 + 57.6 ≈ 158。对上了。

三、一个更麻烦的细节

第一反应是用 CSS 处理掉空段落:

.gh-content > p:empty { display: none; }

结果发现有的页面好了,有的没好。原因在于 :empty 这个选择器——它要求元素完全没有任何子节点,而浏览器把空白字符也算作文本节点。也就是说:

<p></p>            ← 匹配 :empty
<p>        </p>    ← 只有空格,不匹配 :empty

两篇文章里,一篇开头是真空段落,另一篇开头是带空格的段落,所以表现不一致。(Selectors Level 4 里的 :blank 本来是为了解决这个的,但浏览器一直没有实现。)

四、最后的做法

纯 CSS 没法判断「这个段落里有没有可见文字」,所以改用几行 JS,在页面加载时把正文开头连续的空段落删掉:

var content = document.getElementById('post-content');
if (content) {
    var blank = content.firstElementChild;
    while (blank && blank.tagName === 'P'
           && !blank.textContent.trim()
           && !blank.querySelector('img, video, iframe, figure')) {
        var after = blank.nextElementSibling;
        blank.remove();
        blank = after;
    }
}

只处理开头,碰到第一个有内容的元素就停,所以不会误删正文中间那些有意为之的空行。querySelector 那个判断是防止把「只放了一张图的段落」当成空段落删掉。

同时补两条 CSS 兜底:

.gh-content > *:first-child,
.gh-content > p:empty + * { margin-top: 0; }

再把两处 padding 收紧(44→28、56→34),最后署名到正文第一行是 62 像素,正常了。

五、小结

外边距塌陷大概是 CSS 里最容易让人困惑的机制。它其实是为了让段落间距看起来自然——相邻段落的上下边距不会叠加成双倍。但它的副作用值得记住:

  • 给父元素加 padding 或 border 就能挡住塌陷
  • 给父元素加 overflow: hidden 或 display: flow-root 也行
  • 空元素会让上下边距直接穿透,这是最容易踩的一种

遇到「莫名其妙多出来一块空白」,先量尺寸、把数值拆成能对上号的几段,然后往「是不是塌陷了」这个方向想,通常比翻样式表快得多。

评论