> ## Content Index
> Fetch the complete content index at: https://blog.20061124.xyz/llms.txt
> Use this file to discover other available public pages before exploring further.

# CSS 外边距塌陷：一个空段落引发的排版 bug
- URL: https://blog.20061124.xyz/css-margin-collapse/
- Published: 2026-09-26T02:00:00.000Z
- Updated: 2026-10-01T06:56:57.000Z
- Description: 署名和正文之间莫名空出 158 像素。量一量、拆一拆，根因是空段落让标题的上边距塌了出去。
- Author: Ray Jack
- Tags: 前端

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

一开始以为是 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` 也行
- 空元素会让上下边距直接穿透，这是最容易踩的一种

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