Skip to content

BFC 与层叠上下文

一、块级格式化上下文(BFC)

什么是 BFC

BFC(Block Formatting Context,块级格式化上下文)是 CSS 可视化渲染的一个核心概念。它是页面中的一块独立的渲染区域,内部的元素布局不会影响外部元素,外部元素的布局也不会影响内部。

可以把 BFC 理解为一个"结界"——结界内部自成体系,与外界互不干扰。

BFC 属于 CSS 规范中 Visual Formatting Model 的一部分。除了 BFC,还有 IFC(Inline Formatting Context)、GFC(Grid Formatting Context)、FFC(Flex Formatting Context)等格式化上下文,它们分别定义了不同类型元素的布局规则。

BFC 的布局规则

  1. 内部的 Box 会在垂直方向上一个接一个地放置
  2. 同一个 BFC 内相邻 Box 的垂直 margin 会发生折叠(collapse)
  3. BFC 的区域不会与浮动元素重叠
  4. BFC 是一个独立的容器,内外元素互不影响
  5. 计算 BFC 高度时,浮动子元素也参与计算

这 5 条规则中,第 2、3、5 条是最常考的面试考点,也是 BFC 解决实际问题的关键。

如何触发 BFC

以下任意一个条件均可创建新的 BFC:

条件示例
根元素<html>
float 不为 nonefloat: left / float: right
positionabsolutefixedposition: absolute
overflow 不为 visibleoverflow: hidden / auto / scroll
displayinline-blockdisplay: inline-block
displayflexinline-flexdisplay: flex
displaygridinline-griddisplay: grid
displaytable-celltable-captiondisplay: table-cell
displayflow-rootdisplay: flow-root(专为创建 BFC 设计)
containlayoutcontentpaintcontain: layout
多列容器column-countcolumn-width 不为 auto

最佳实践:当你需要创建 BFC 但不希望产生其他副作用时,使用 display: flow-root——这是 CSS 专门为创建无副作用 BFC 设计的值。

css
.bfc-container {
  display: flow-root; /* 最干净的 BFC 触发方式 */
}

对比其他方式的副作用:

触发方式副作用
overflow: hidden超出内容被裁剪
float: left元素脱离文档流,影响兄弟元素
position: absolute元素脱离文档流
display: inline-block改变元素的显示类型
display: flow-root无副作用

BFC 解决的三大经典问题

问题一:margin 折叠(Margin Collapsing)

现象:同一个 BFC 内,相邻块级元素的垂直 margin 会合并为较大的那个值。

html
<div class="container">
  <div class="box-a">Box A (margin-bottom: 30px)</div>
  <div class="box-b">Box B (margin-top: 20px)</div>
</div>
css
.box-a { margin-bottom: 30px; }
.box-b { margin-top: 20px; }
预期间距:30 + 20 = 50px
实际间距:max(30, 20) = 30px  ← margin 折叠!

┌─────────────┐
│    Box A     │
└─────────────┘
      ↕ 30px(不是 50px)
┌─────────────┐
│    Box B     │
└─────────────┘

折叠规则的完整总结

场景是否折叠结果
相邻兄弟元素max(margin-bottom, margin-top)
父元素与第一个子元素(无 border/padding 隔开)父子的 margin-top 合并
父元素与最后一个子元素(无 border/padding 隔开)父子的 margin-bottom 合并
空块级元素(无 height/border/padding)自身 margin-top 和 margin-bottom 合并
两个正 margin折叠取较大值
两个负 margin折叠取绝对值较大的负值
一正一负折叠两者相加

用 BFC 解决 margin 折叠

让相邻元素处于不同的 BFC 中,它们的 margin 就不会折叠:

html
<div class="container">
  <div class="box-a">Box A</div>
  <div class="bfc-wrapper">
    <div class="box-b">Box B</div>
  </div>
</div>
css
.bfc-wrapper {
  display: flow-root; /* 创建新的 BFC */
}
┌─────────────┐
│    Box A     │
└─────────────┘
      ↕ 50px(30 + 20,不再折叠)
┌ ─ ─ ─ ─ ─ ─ ┐  ← BFC 边界
│┌─────────────┐│
││    Box B     ││
│└─────────────┘│
└ ─ ─ ─ ─ ─ ─ ┘

父子 margin 折叠与解决

html
<div class="parent">
  <div class="child">Child</div>
</div>
css
.parent { background: #eee; }
.child  { margin-top: 50px; }
期望:                    实际:
┌──────────────┐         
│              │ ← 50px   ↕ 50px(margin 穿透到父元素外面)
│ ┌──────────┐ │         ┌──────────────┐
│ │  Child   │ │         │ ┌──────────┐ │
│ └──────────┘ │         │ │  Child   │ │
│              │         │ └──────────┘ │
└──────────────┘         └──────────────┘

子元素的 margin-top "穿透"了父元素,表现为父元素整体下移。这是因为父子元素处于同一个 BFC 中,且父元素没有 border-toppadding-top 来隔开。

解决方案(任选其一):

css
/* 方案一:给父元素创建 BFC */
.parent { display: flow-root; }

/* 方案二:给父元素加 border 或 padding 隔开 */
.parent { border-top: 1px solid transparent; }
.parent { padding-top: 1px; }

/* 方案三:父元素加 overflow */
.parent { overflow: hidden; }

问题二:清除浮动(高度塌陷)

现象:当子元素全部浮动时,父容器的高度会塌陷为 0,因为浮动元素脱离了文档流。

html
<div class="container">
  <div class="float-item">Float 1</div>
  <div class="float-item">Float 2</div>
</div>
<div class="next-section">Next Section</div>
css
.float-item { float: left; width: 200px; height: 100px; }
期望:                       实际:
┌──────────────────┐        ┌┐ ← 父容器高度为 0
│ ┌────┐ ┌────┐    │        └┘
│ │ F1 │ │ F2 │    │        ┌────┐ ┌────┐
│ └────┘ └────┘    │        │ F1 │ │ F2 │  ← 浮动元素溢出
│                  │        └────┘ └────┘
└──────────────────┘        ┌──────────────────┐
┌──────────────────┐        │   Next Section   │ ← 与浮动元素重叠
│   Next Section   │        └──────────────────┘
└──────────────────┘

BFC 规则第 5 条:计算 BFC 高度时,浮动子元素也参与计算。所以给父容器创建 BFC 就能解决高度塌陷。

解决方案

css
/* 方案一:display: flow-root(最推荐) */
.container { display: flow-root; }

/* 方案二:overflow: hidden(经典方案,但超出内容会被裁剪) */
.container { overflow: hidden; }

/* 方案三:clearfix 伪元素(兼容老浏览器) */
.container::after {
  content: "";
  display: block;
  clear: both;
}

clearfix 的原理clear: both 使伪元素移到所有浮动元素下方,伪元素作为非浮动的块级元素参与父容器高度计算,从而"撑起"父容器。

问题三:浮动元素覆盖

现象:浮动元素会覆盖相邻的非浮动元素。

html
<div class="float-box">Float</div>
<div class="normal-box">Normal text that wraps around the float...</div>
css
.float-box  { float: left; width: 100px; height: 100px; }
.normal-box { /* 没有特殊处理 */ }
┌──────┐ Normal text that
│Float │ wraps around the
│      │ float element...
└──────┘ More text here...
↑ normal-box 的背景区域实际上被 float 覆盖了

BFC 规则第 3 条:BFC 的区域不会与浮动元素重叠。

css
.normal-box { display: flow-root; } /* 创建 BFC */
┌──────┐ ┌─────────────────────┐
│Float │ │ Normal text inside  │
│      │ │ BFC, does not wrap  │
└──────┘ │ around the float.   │
         └─────────────────────┘

这也是实现两栏自适应布局的经典方案:左栏 float: left,右栏 overflow: hidden(触发 BFC)。


二、层叠上下文(Stacking Context)

什么是层叠上下文

层叠上下文(Stacking Context)是 HTML 元素的一个三维概念——除了 x 轴和 y 轴上的位置,元素还有 z 轴上的层叠顺序。层叠上下文决定了元素在 z 轴方向上的渲染顺序

可以把层叠上下文想象成一个"图层组"——Photoshop 中的图层组有自己内部的层叠顺序,整个组作为一个整体参与外部的层叠排序。

                z 轴(朝向屏幕)


                 │  ┌──────────┐  z-index: 2
                 │  │          │
                 │  └──────────┘
                 │     ┌──────────┐  z-index: 1
                 │     │          │
                 │     └──────────┘
                 │        ┌──────────┐  z-index: 0
    用户视角 ←── │        │          │
                 │        └──────────┘

如何创建层叠上下文

以下条件可以创建新的层叠上下文:

条件说明
根元素 <html>页面默认的层叠上下文
positionstaticz-index 不为 auto最经典的触发方式
position: fixedposition: sticky始终创建层叠上下文
opacity 小于 1opacity: 0.99 就会创建
transform 不为 nonetransform: translateZ(0)
filter 不为 nonefilter: blur(0)
perspective 不为 none3D 透视
clip-path 不为 none裁剪路径
mask / mask-image 不为 none遮罩
isolation: isolate专门用于创建层叠上下文
will-change 值为上述任意属性will-change: transform
contain: layoutcontain: paintCSS Containment
Flex/Grid 子元素且 z-index 不为 autoFlex/Grid 项目的特殊规则

容易踩坑的点opacitytransformfilter 等属性会"意外"创建层叠上下文,导致 z-index 的表现不符合预期。

层叠顺序(Stacking Order)

在同一个层叠上下文中,元素的渲染顺序从底到顶是:

┌──────────────────────────────────────────┐
│  ⑦ z-index > 0                    最上层  │
├──────────────────────────────────────────┤
│  ⑥ z-index: 0 / auto                    │
│    (或不依赖 z-index 的层叠上下文)        │
├──────────────────────────────────────────┤
│  ⑤ inline / inline-block 元素            │
├──────────────────────────────────────────┤
│  ④ float 浮动元素                         │
├──────────────────────────────────────────┤
│  ③ block 块级元素                         │
├──────────────────────────────────────────┤
│  ② 负 z-index                            │
├──────────────────────────────────────────┤
│  ① 层叠上下文的 background / border 最底层 │
└──────────────────────────────────────────┘

这个 7 层顺序是面试中的高频考点。记忆口诀:背(border) → 负 → 块 → 浮 → 行 → 零 → 正

关键理解

  • inline 元素的层级比 block 高——因为行内元素通常承载文字内容,W3C 认为内容比装饰更重要
  • float 元素的层级在 block 和 inline 之间——float 的设计初衷是文字环绕图片
  • z-index: 0 和 z-index: auto 的区别z-index: 0 会创建新的层叠上下文,auto 不会

z-index 的作用范围

z-index 只在同一个层叠上下文内进行比较。不同层叠上下文中的元素,无论 z-index 多大,都不能跨越层叠上下文的边界。

html
<div class="parent-a" style="position: relative; z-index: 1;">
  <div class="child-a" style="position: relative; z-index: 9999;">A</div>
</div>
<div class="parent-b" style="position: relative; z-index: 2;">
  <div class="child-b" style="position: relative; z-index: 1;">B</div>
</div>
结果:child-b 在 child-a 上面!

因为 parent-a (z-index:1) < parent-b (z-index:2)
child-a 的 z-index:9999 只在 parent-a 内有效,
无法超越 parent-b 的层叠上下文。

         ┌──────────────────────┐
         │  parent-b (z:2)      │
         │  ┌──────────────┐    │  ← 在上面
         │  │ child-b (z:1)│    │
         │  └──────────────┘    │
         └──────────────────────┘
    ┌──────────────────────┐
    │  parent-a (z:1)      │
    │  ┌────────────────┐  │  ← 在下面
    │  │child-a (z:9999)│  │
    │  └────────────────┘  │
    └──────────────────────┘

这是 z-index 最常见的困惑来源——"为什么我的 z-index: 9999 不生效?"答案几乎总是:它的父元素创建了一个较低的层叠上下文

z-index 的常见误区

误区一:z-index 对所有元素生效

z-index 只对定位元素position 不为 static)和 Flex/Grid 子元素生效。对普通文档流元素设置 z-index 无效。

css
/* ❌ 无效 */
.normal-element {
  z-index: 100; /* position 是默认的 static,z-index 不生效 */
}

/* ✅ 有效 */
.positioned-element {
  position: relative;
  z-index: 100;
}

误区二:z-index: auto 和 z-index: 0 是一样的

视觉上它们的层级相同(都在第 ⑥ 层),但行为不同:

  • z-index: auto不创建新的层叠上下文
  • z-index: 0创建新的层叠上下文
css
.parent {
  position: relative;
  z-index: auto; /* 不创建层叠上下文,子元素可以"逃出"与外部元素比较 z-index */
}

.parent {
  position: relative;
  z-index: 0; /* 创建层叠上下文,子元素被"封印"在内部 */
}

误区三:负 z-index 可以让元素隐藏到任何元素后面

z-index 只能让元素排在其所在层叠上下文的 background 后面,不能穿透到更外层的层叠上下文。

css
.stacking-context {
  position: relative;
  z-index: 0; /* 创建层叠上下文 */
  background: white;
}

.child {
  position: relative;
  z-index: -1; /* 在 .stacking-context 的 background 下面 */
  /* 但仍然在 .stacking-context 的父级之上 */
}

三、实际开发中的典型场景

场景一:Modal 弹窗的 z-index 管理

大型应用中经常出现多个弹窗层叠的问题。最佳实践是建立 z-index 分级体系:

css
:root {
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-fixed: 300;
  --z-modal-backdrop: 400;
  --z-modal: 500;
  --z-popover: 600;
  --z-tooltip: 700;
  --z-toast: 800;
}

.modal-backdrop { z-index: var(--z-modal-backdrop); }
.modal          { z-index: var(--z-modal); }
.tooltip        { z-index: var(--z-tooltip); }

关键策略:避免在组件中硬编码 z-index,使用 CSS 变量统一管理。

场景二:position: fixed 元素被 transform 父元素"困住"

html
<div class="transformed-parent" style="transform: translateX(0);">
  <div class="fixed-child" style="position: fixed; top: 0;">
    This won't be fixed to viewport!
  </div>
</div>

transform 不为 none 的元素会为其后代创建一个新的包含块(Containing Block)。这意味着 position: fixed 的子元素不再相对于视口定位,而是相对于 transform 父元素定位。

这是一个非常隐蔽的 bug。filterperspectivewill-change: transform 也有同样的效果。

解决方案:将 fixed 元素移到 transform 父元素外面(如 Portal 到 body)。

场景三:opacity 导致 z-index 失效

html
<div class="parent" style="opacity: 0.99;">
  <div class="child" style="position: relative; z-index: 100;">
    This z-index only works within parent's stacking context
  </div>
</div>

opacity < 1 会创建新的层叠上下文,把子元素的 z-index "封印"在内部。动画中使用 opacity 渐变时尤其容易踩坑。

场景四:isolation: isolate 的妙用

当你需要让元素创建层叠上下文但不想产生其他副作用时:

css
.component {
  isolation: isolate; /* 创建层叠上下文,无其他副作用 */
}

这在组件库开发中非常有用——让每个组件创建独立的层叠上下文,避免内部的 z-index 泄漏到外部。


四、格式化上下文全景图

BFC 只是 CSS 格式化上下文的一种,完整的格式化上下文体系:

格式化上下文全称触发方式布局规则
BFCBlock Formatting Contextoverflowfloatdisplay: flow-root块级元素垂直排列,margin 折叠,浮动计算
IFCInline Formatting Context块级元素内部只有行内元素时自动创建行内元素水平排列,由 line-heightvertical-align 控制
FFCFlex Formatting Contextdisplay: flex / inline-flexFlex 布局规则,主轴 + 交叉轴
GFCGrid Formatting Contextdisplay: grid / inline-gridGrid 布局规则,行轨道 + 列轨道

理解:Flex 和 Grid 容器天然就是 BFC——它们的内部不会与外部发生 margin 折叠,也能包裹浮动元素。这就是为什么使用现代布局方案后,你几乎不再需要手动处理浮动和 margin 折叠问题。


经典面试题解析

题目一:什么是 BFC?如何触发?解决了什么问题?

标准回答结构

  1. 定义:BFC 是一个独立的渲染区域,内部布局不影响外部
  2. 触发方式overflow: hiddendisplay: flow-root(最佳)、floatposition: absolute、Flex/Grid 等
  3. 解决的三大问题
    • margin 折叠:让元素处于不同的 BFC
    • 高度塌陷:BFC 计算高度时包含浮动子元素
    • 浮动覆盖:BFC 不与浮动元素重叠

题目二:为什么 margin 会折叠?如何解决?

折叠条件(必须同时满足):

  1. 必须是块级元素
  2. 必须在同一个 BFC
  3. 必须是垂直方向的 margin(水平方向不会折叠)
  4. 之间没有非空内容、padding、border隔开

解决方案

  • 让元素处于不同的 BFC(display: flow-root
  • paddingborder 隔开
  • 使用 Flex/Grid 布局(子元素不会发生 margin 折叠)

题目三:下面代码中元素的层叠顺序是什么?

html
<div class="a" style="position: relative; z-index: 1;">
  <div class="a-child" style="position: relative; z-index: 100;">A-child</div>
</div>
<div class="b" style="position: relative; z-index: 2;">
  <div class="b-child" style="position: relative; z-index: -1;">B-child</div>
</div>

分析

  1. .a(z-index: 1)和 .b(z-index: 2)是兄弟元素,各自创建层叠上下文
  2. .b > .a,因为 z-index 2 > 1
  3. .a-child(z-index: 100)被封印在 .a 内部,无论多大都不能超越 .b
  4. .b-child(z-index: -1)在 .b 的 background 之下,但仍在 .a 之上

最终顺序(从底到顶):.a 背景 → .a-child.b 背景 → .b-child

等等——.b-child 的 z-index 是 -1,它在 .b 的 background 之下?不对,再看:

.b-child 的 z-index: -1 是在 .b 这个层叠上下文内,排在 .b 的 background 之下。但由于 .b 整体在 .a 之上,所以 .b-child 虽然 z-index 为负,但其实际渲染仍然在 .a 的所有内容之上。

最终从底到顶.a 背景 → .a-child.b-child.b 内容

题目四:为什么 z-index: 9999 不生效?

99% 的情况是以下原因之一:

  1. 没有设置 positionz-index 只对定位元素有效
  2. 父元素创建了低层级的层叠上下文:子元素被"封印"在内部
  3. 其他属性意外创建了层叠上下文opacitytransformfilter

排查步骤

  1. 检查目标元素是否有 position 设置
  2. 沿 DOM 树向上检查每个祖先元素是否创建了层叠上下文
  3. 在 Chrome DevTools 的 Layers 面板中查看层叠情况

题目五:清除浮动有哪些方式?各自的原理是什么?

方式原理推荐程度
display: flow-root创建 BFC,浮动子元素参与高度计算⭐⭐⭐⭐⭐
overflow: hidden/auto创建 BFC(同上),但有副作用⭐⭐⭐⭐
clearfix 伪元素clear: both 让伪元素移到浮动下方,撑起父容器⭐⭐⭐⭐
在末尾加空 <div style="clear:both">同 clearfix 原理,但增加无语义 DOM⭐⭐
父元素也浮动创建 BFC,但影响布局

追问思考

  • display: flow-root 是什么时候加入 CSS 标准的?在此之前,创建"无副作用的 BFC"最好的方式是什么?
  • Flex 容器和 Grid 容器是否是 BFC?为什么使用现代布局后很少需要手动处理 margin 折叠?
  • isolation: isolate 的底层原理是什么?它和 z-index: 0 有什么区别?
  • 为什么 inline 元素的层叠顺序比 block 高?这个设计背后的逻辑是什么?
  • 在 React/Vue 的 Portal 组件中,层叠上下文会带来什么挑战?如何解决?

用心学习,用代码说话 💻