CSS 定位与层叠上下文

系统掌握 CSS position 属性的五个值、包含块的确定规则、层叠上下文的形成条件,以及 z-index 的层叠机制。

CSS 定位与层叠上下文

为什么定位是 CSS 布局的精髓

CSS 的布局模型经历了从 Normal Flow(标准流)到 FlexboxGrid 的演进,但 position 属性始终是控制元素空间关系的核心工具。一个元素是否脱离文档流、相对于哪个参照物定位、在层叠顺序中处于什么位置——这些都取决于 positionz-index 的交互规则。

理解定位不仅仅是"知道 fixed 是相对于视口定位"那么简单。包含块的确定规则、层叠上下文(Stacking Context)的形成条件、position: sticky 的实际表现——这些细节决定了你在复杂布局中能否准确控制元素的位置。

面试定位:CSS 定位是面试的经典考点。面试官通过包含块的确定规则、层叠上下文的形成条件、z-index 的层叠比较方式等问题,判断候选人对 CSS 渲染机制的深层理解。


position 属性的五个值

static —— 默认值

position: static 是所有元素的默认值。处于正常文档流中,toprightbottomleftz-index 属性无效。

.box {
  position: static; /* 默认值,无需显式声明 */
}

relative —— 相对定位

相对于元素自身的原始位置进行偏移,不脱离文档流。元素仍然占据原始空间,其他元素不会填补空缺。

.box {
  position: relative;
  top: 20px;    /* 相对于原始顶部向下偏移 20px */
  left: 10px;   /* 相对于原始左侧向右偏移 10px */
}

视觉表现

原始位置        偏移后位置
┌────────┐         ┌────────┐
│   A    │         │   A'   │
└────────┘         └────────┘
   ↑
   └─ 原位置仍然保留,其他元素不会进入

absolute —— 绝对定位

相对于包含块(Containing Block)进行定位,完全脱离文档流。元素不再占据空间,其他元素会填补空缺。

.parent {
  position: relative; /* 设为包含块 */
}

.child {
  position: absolute;
  top: 0;
  right: 0;
  /* 相对于 .parent 的内边距框右上角定位 */
}

fixed —— 固定定位

相对于视口(Viewport)进行定位,完全脱离文档流。元素不随页面滚动而移动。

.modal-backdrop {
  position: fixed;
  inset: 0; /* 覆盖整个视口 */
  background: rgba(0, 0, 0, 0.5);
}

注意:在 CSS Transforms、Perspective 等场景下,position: fixed 的包含块会变为最近的变换祖先,而非视口。

sticky —— 粘性定位

相对于最近的可滚动祖先(nearest scrollable ancestor)进行定位,在跨越阈值前表现为 relative,之后表现为 fixed

.sticky-header {
  position: sticky;
  top: 0; /* 滚动出视口前距离顶部 0px */
}

兼容性问题sticky 在很多场景下表现良好,但旧版 Safari(< 13)和某些 Android 浏览器需要 -webkit-sticky 前缀。


包含块(Containing Block)的确定规则

包含块是定位的参照物,它的确定规则是理解定位机制的关键。

规则速查表

position 值 包含块
static / relative 最近块级祖先的内容框
absolute 最近定位祖先(positionstatic)的内边距框
fixed 视口(除非在 transform/perspective 等场景下)
sticky 最近可滚动祖先的视口

详细解释

1. 对于 static 和 relative

包含块是最近块级祖先的内容框(content box of the nearest block-level ancestor)。

<div style="padding: 20px; border: 1px solid black;">
  <!-- 包含块是这个 div 的内容框 -->
  <p style="position: relative; top: 10px;">相对定位</p>
</div>

2. 对于 absolute

包含块是最近定位祖先的内边距框(padding box of the nearest positioned ancestor)。

<div style="position: relative; padding: 20px;">
  <!-- 包含块是这个 div 的内边距框 -->
  <p style="position: absolute; top: 0; left: 0;">
    相对于 padding 外边缘定位
  </p>
</div>

3. 对于 fixed

通常是视口。但在以下场景中,包含块会变为最近的可变形祖先(transforming ancestor):

  • 祖先元素有 transform(非 none
  • 祖先元素有 perspective(非 none
  • 祖先元素有 transform-style: preserve-3d
  • 祖先元素有 will-change: transform

这意味着在使用了 CSS 3D 变换的页面中,position: fixed 可能不会相对于视口定位。


层叠上下文(Stacking Context)

什么是层叠上下文

层叠上下文是一个三维概念——它将页面在垂直于屏幕的方向(z 轴)上划分为多个独立的层。在同一个层叠上下文内,元素按照层叠顺序排列;不同层叠上下文之间,只有祖先能够遮盖后代。

形成层叠上下文的条件

以下属性会创建新的层叠上下文:

属性 示例
根元素 <html>
positionstatic + z-indexauto position: relative; z-index: 1
position: fixed / sticky 始终创建(相当于 z-index: auto
z-indexauto 的 Flex 子项 display: flex; z-index: 1
z-indexauto 的 Grid 子项 display: grid; z-index: 1
opacity < 1 opacity: 0.9
transformnone transform: scale(1)
filternone filter: blur(1px)
perspectivenone perspective: 100px
will-change 指定了上述属性 will-change: transform
isolation: isolate 强制创建新的层叠上下文

关键点position: fixed 总是创建层叠上下文,即使没有设置 z-index

Painting Order(层叠顺序)

在同一个层叠上下文中,从后到前的顺序是:

1. 背景和边框(层叠上下文根元素的背景和边框)
2. z-index < 0 的子元素(负 z-index)
3. 块级子元素(Block-level children)
4. 浮动子元素(Float children)
5. 行内子元素(Inline children)
6. z-index: auto / z-index: 0 的子元素
7. z-index > 0 的子元素(正 z-index)

z-index 的层叠规则

核心规则:z-index 只在同一个层叠上下文内才有意义。不同层叠上下文之间,祖先的 z-index 决定了谁能遮盖谁。

层叠上下文 A (z-index: 10)
├── 子元素 A1 (z-index: 1000)  ← 在上下文 A 内部排序
└── 子元素 A2 (z-index: 5)

层叠上下文 B (z-index: 9)
└── 子元素 B1 (z-index: 9999)

结果:A1 和 A2 都在上下文 A 内部
      B1 在上下文 B 内
      上下文 A (z-index: 10) 遮盖上下文 B (z-index: 9)
      所以 A1 和 A2 都可见,B1 被遮盖

常见错误:在不同的层叠上下文中设置很大的 z-index 值,希望覆盖其他组件。这不会生效——只有祖先层叠上下文的 z-index 决定遮盖关系。


position: absolute vs fixed 的关键差异

特性 absolute fixed
包含块 最近的定位祖先 视口(正常情况)/变换祖先
随页面滚动
创建层叠上下文 是(当有 z-index 时) 总是
移动端 Safari 正常 可能在输入框聚集时"飞走"

fixed 的"飞走"问题

在 iOS Safari(以及某些 Android 浏览器)中,当页面有软键盘弹出时,position: fixed 元素可能异常移动。这是 Safari 的实现 bug,常见解决方案:

/* 使用 sticky 代替 fixed(如果适用) */
.sticky-header {
  position: sticky;
  top: 0;
}

/* 或使用 JavaScript 检测键盘状态并切换定位方式 */

position: sticky 的实际表现

粘性定位的三种场景

1. 粘性头部

thead {
  position: sticky;
  top: 0;
  background: white;
}

2. 粘性侧边栏

.sidebar {
  position: sticky;
  top: 20px; /* 距离视口顶部 20px 时开始粘 */
  align-self: flex-start;
}

3. 粘性子标题

.section-header {
  position: sticky;
  top: 60px; /* 在滚动列表中保持可见 */
}

sticky 的限制

  • 需要指定 topbottomleftright 之一,否则等同于 relative
  • 父元素不能有 overflow: hiddenoverflow: auto(会破坏粘性)
  • 表格相关元素(theadtbody)在某些浏览器中有兼容性问题

实战场景

场景一:对话框(Modal)的正确实现

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000; /* 确保在最上层 */
}

.modal-content {
  position: relative;
  background: white;
  padding: 2rem;
  border-radius: 8px;
  z-index: 1001; /* 比 backdrop 高,但必须在同一层叠上下文 */
}

注意:很多组件库的错误做法是在 .modal-content 上设置很高的 z-index。但如果 .modal-backdrop.modal-content 的父元素创建了新的层叠上下文,子元素的 z-index 可能无法超过父元素。

场景二:下拉菜单(Dropdown)

.dropdown {
  position: relative;
}

.dropdown-menu {
  position: absolute;
  top: 100%; /* 出现在触发元素下方 */
  left: 0;
  min-width: 200px;
  background: white;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  z-index: 100; /* 确保在其他内容之上 */
}

场景三:Sticky Footer

.page-wrapper {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

.main-content {
  flex: 1;
}

.footer {
  position: sticky;
  bottom: 0; /* 始终贴在视口底部 */
}

面试高频问题

Q: position: absolute 和 fixed 的区别?

回答要点:两者都脱离文档流。区别在于包含块:absolute 相对于最近定位祖先的内边距框定位,fixed 相对于视口定位(但在 transform 等场景下会降级为变换祖先)。另外 fixed 不随页面滚动,而 absolute 会随滚动移动。

Q: 什么是层叠上下文?如何形成?

回答要点:层叠上下文是浏览器将页面在 z 轴方向划分为的独立层。形成条件包括:根元素、position 非 static + z-index 非 auto、opacity 小于 1、transform 非 none、filter 非 none 等。关键规则是:z-index 只在同一层叠上下文内有意义。

Q: z-index 为什么有时候不生效?

回答要点:常见原因是父元素创建了新的层叠上下文,子元素的 z-index 只在该上下文内部比较,无法超过另一个层叠上下文祖先。另一个原因是使用了 position: static(不支持 z-index)。

Q: position: sticky 的原理是什么?

回答要点:sticky 相对于最近可滚动祖先的视口定位。在阈值(top/left 等)之前表现为 relative,滚动超过阈值后表现为 fixed。限制包括:父元素不能有 overflow: hidden/auto、需要指定阈值方向。


延展阅读