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
position 为 absolute 或 fixedposition: absolute
overflow 不为 visibleoverflow: hidden / auto / scroll
display 为 inline-blockdisplay: inline-block
display 为 flex 或 inline-flexdisplay: flex
display 为 grid 或 inline-griddisplay: grid
display 为 table-cell、table-captiondisplay: table-cell
display 为 flow-rootdisplay: flow-root(专为创建 BFC 设计)
contain 为 layout、content、paintcontain: layout
多列容器column-count 或 column-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-top、padding-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>页面默认的层叠上下文
position 非 static 且 z-index 不为 auto最经典的触发方式
position: fixed 或 position: 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: layout 或 contain: paintCSS Containment
Flex/Grid 子元素且 z-index 不为 autoFlex/Grid 项目的特殊规则

容易踩坑的点:opacity、transform、filter 等属性会"意外"创建层叠上下文,导致 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。filter、perspective、will-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 Contextoverflow、float、display: flow-root 等块级元素垂直排列,margin 折叠,浮动计算
IFCInline Formatting Context块级元素内部只有行内元素时自动创建行内元素水平排列,由 line-height 和 vertical-align 控制
FFCFlex Formatting Contextdisplay: flex / inline-flexFlex 布局规则,主轴 + 交叉轴
GFCGrid Formatting Contextdisplay: grid / inline-gridGrid 布局规则,行轨道 + 列轨道

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


经典面试题解析 ​

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

标准回答结构:

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

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

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

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

解决方案:

  • 让元素处于不同的 BFC(display: flow-root)
  • 加 padding 或 border 隔开
  • 使用 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. 没有设置 position:z-index 只对定位元素有效
  2. 父元素创建了低层级的层叠上下文:子元素被"封印"在内部
  3. 其他属性意外创建了层叠上下文:opacity、transform、filter 等

排查步骤:

  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 组件中,层叠上下文会带来什么挑战?如何解决?

用心学习,用代码说话 💻