CSS 定位与层叠上下文
为什么定位是 CSS 布局的精髓
CSS 的布局模型经历了从 Normal Flow(标准流)到 Flexbox、Grid 的演进,但 position 属性始终是控制元素空间关系的核心工具。一个元素是否脱离文档流、相对于哪个参照物定位、在层叠顺序中处于什么位置——这些都取决于 position 和 z-index 的交互规则。
理解定位不仅仅是"知道 fixed 是相对于视口定位"那么简单。包含块的确定规则、层叠上下文(Stacking Context)的形成条件、position: sticky 的实际表现——这些细节决定了你在复杂布局中能否准确控制元素的位置。
面试定位:CSS 定位是面试的经典考点。面试官通过包含块的确定规则、层叠上下文的形成条件、z-index 的层叠比较方式等问题,判断候选人对 CSS 渲染机制的深层理解。
position 属性的五个值
static —— 默认值
position: static 是所有元素的默认值。处于正常文档流中,top、right、bottom、left、z-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 |
最近定位祖先(position 非 static)的内边距框 |
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> |
position 非 static + z-index 非 auto |
position: relative; z-index: 1 |
position: fixed / sticky |
始终创建(相当于 z-index: auto) |
z-index 非 auto 的 Flex 子项 |
display: flex; z-index: 1 |
z-index 非 auto 的 Grid 子项 |
display: grid; z-index: 1 |
opacity < 1 |
opacity: 0.9 |
transform 非 none |
transform: scale(1) |
filter 非 none |
filter: blur(1px) |
perspective 非 none |
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 的限制
- 需要指定
top、bottom、left或right之一,否则等同于relative - 父元素不能有
overflow: hidden或overflow: auto(会破坏粘性) - 表格相关元素(
thead、tbody)在某些浏览器中有兼容性问题
实战场景
场景一:对话框(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、需要指定阈值方向。