浮动(float)与定位(position)都是"对文档流的干预机制",它们的出现正是因为早期 Web 页面仅靠文档流无法完成复杂的布局需求。
问题背景:文档流的局限
起点:原始的"自然流布局"
最早的 HTML + CSS(90年代中期)只有:
- 块级元素(垂直排布);
- 行内元素(水平排布)。
这种布局叫 普通文档流(Normal Flow) 。
它非常简单,却也非常有限:
想实现的效果 | 文档流能做到吗? |
---|---|
图片左侧文字环绕 | ❌ 不能 |
两栏、三栏布局 | ❌ 不能 |
固定一个导航栏 | ❌ 不能 |
自由悬浮广告 | ❌ 不能 |
人们需要更强的"空间控制能力"。
于是,浮动(float)与定位(position) 诞生了。
浮动(Float):从文字环绕到布局神器
设计动机------为什么会有"浮动"?
要理解 float,必须回到 1990s 的网页初期。
那时,网页主要是新闻、论文、博客一类的"文本文档"。
想象一下这样的排版目标:
"我要让一张小图片靠在左边,旁边的文字自然环绕它。"
HTML 提供了 <img>
,但 <img>
在默认文档流中是 行内元素 ,
它只能嵌在文字行里,大小会破坏文字行高,不能让文字优雅环绕。
于是,人们希望:
"能不能让这张图片暂时脱离原来的行列,靠在一边漂着,
但仍让文字自动绕开它,不要重叠?"
这,就是 float(浮动) 的原始语义 ------
"让盒子从文档流中漂浮出来,但文字仍能感知它。"
浮动在 BFC 层级中的位置
我们可以把 BFC 内部想象成一个三层结构:
plain
BFC(块级格式化上下文)
├── 浮动层(float layer) ← 浮动元素在此排布
├── 常规流层(normal flow layer) ← 普通块与文字
└── 绝对定位层(absolute layer) ← 完全脱流元素
浮动层是在文档流之上,但仍属于当前 BFC 。
所以它既能影响文字流(因为在 BFC 内),
又不会影响块流的垂直堆叠(因为脱离普通流)。
浮动的算法(简化版)
以 float:left
为例,浏览器布局算法可抽象为:
1️⃣ 创建浮动盒,测量其宽高;
2️⃣ 在当前 BFC 的左边缘寻找可放置位置 ;
3️⃣ 调整行盒的"可用空间矩形",避开浮动盒;
4️⃣ 行盒中内容重新流动、换行;
5️⃣ 若后续浮动盒出现,依次排列在可用空间下方。
浮动前后变化
浮动前(普通流):
plain
BFC
├── p
│── p
│── p
│── float1
│── p
│── p
└── p
视觉效果:
plain
文字文字文字文字文字
文字文字文字文字文字
文字文字文字文字文字
|float1|
| |
文字文字文字文字文字
文字文字文字文字文字
文字文字文字文字文字
浮动后(浮动层介入):
plain
BFC
│── p
│── p
├── [Float Layer]
│ └── float1(float:left)
│── p
│── p
│── p
└── p
浮动盒 float 迁移到 Float Layer,但它仍被当前 BFC 管辖(因此 BFC 的高度需要计算它)。
视觉效果:
plain
文字文字文字文字文字
文字文字文字文字文字
|float1| 文字文字文字文字文字
| | 文字文字文字文字文字
文字文字文字文字文字
文字文字文字文字文字
浮动的副作用:BFC 高度塌陷
当所有子元素都 float 时,
BFC 的 normal flow 中什么都没有 ,
因此父盒检测不到内容高度:
plain
<div class="parent">
<div class="child"></div>
</div>
plain
.parent { background: lightgray; }
.child { float: left; width: 100px; height: 100px; background: coral; }
结果:
.child
被放到了浮动层;.parent
在 normal flow 层检测不到子盒;- 高度塌陷为 0。
触发 BFC 解决塌陷
当父盒触发 BFC(如 overflow:hidden
)时,
它会:
独立计算内部浮动层的几何边界 ,
将所有浮动盒纳入其包裹范围。
浮动的特性
角度 | 普通流(Normal Flow) | 浮动流(Float Flow) |
---|---|---|
空间关系 | 顺序堆叠 | 贴边,文字避让(顺着边绕着走),不影响BFC内块的垂直排列,宽度允许的情况下会出现块与块的并排 |
排版算法 | 自上而下 | 检测浮动矩形后重新计算行盒 |
所属上下文 | BFC 内部 | BFC 内部(浮动层),不会创建新的上下文 |
高度计算 | 子流自动撑开 | 需 BFC 包裹才能计算 |
本质 | "流体中漂浮的盒子" | "部分脱流的盒子" |
浮动的历史宿命
浮动一度是网页布局的"主力军"(两栏、三栏全靠它),
但它从来就不是为布局设计的。
随着 CSS 发展:
- CSS2.1:引入
position
体系; - CSS3:引入
flexbox
; - CSS4:引入
grid
。
浮动逐渐回归本职:文字环绕与装饰用途。
补充:"视觉流布局" vs "独立容器布局"
前置思考
CSS中很多难以理解的问题,事实上都和块级格式化上下文的延用共享有关,我们的思考惯性是一个盒子内的区局就应该和外界是独立的,有着明确的界限,但CSS的设计却是默认与外界相通,这才导致了Margin穿透与浮动后导致的盒子塌陷,CSS为什么要这么设计?难道不是一个缺陷吗?
事实上,这其实CSS早期遵循的是 "视觉流布局",也就是我们前面提到的纸面化思维,而后来随着发展,"独立容器布局"的思想越来越流行,这正是为什么站在现在容器化的思维回顾 CSS 的早先设计,令人觉得"反人类",这也导致了后来 CSS 被不断的升级(BFC、Flex、Grid)。
CSS 的"最初哲学":视觉流,而非容器流
关键词:连续文本流(Continuous Flow)
CSS1(1996)最初诞生时,目标是------
"让网页像纸上的文字一样排版。"
当时的网页,本质是富文本(Rich Text) ,不是应用界面。
因此,CSS 的底层假设是:
设计出发点 | 说明 |
---|---|
一切排版以文字为核心 | HTML 元素都是文字的容器 |
页面是一张"连续纸张" | 所有盒子共处一个视觉流 |
盒子之间可以自然流动 | 没有明确的"父子隔离" |
块级盒子垂直排列、行内盒子水平排列 | 模仿印刷排版结构 |
盒子间共享上下文 | 保证文字与段落的连续性 |
换句话说 :
在最初的 CSS 眼里,一个网页是"一整张纸",
而不是"许多独立组件拼起来的界面"。
所以:
- 块与块之间 margin 会合并(就像段落之间的间距是"共享"的);
- 浮动能让图片"浮在文字旁",不破坏文字的整体流;
- 没有"盒子边界"这回事,只有"视觉流动的文字"。
这在 1996 年是合理的。问题是,网页在变。
问题的出现:网页从文档 → 应用
进入 2000 年后,网页开始承担复杂布局:
导航栏、侧边栏、卡片、模块、弹窗......
这时候,人们不再希望所有元素共享同一个流 。
他们希望:
- 模块内的排版独立进行;
- 模块之间互不干扰;
- 外部 margin、float 不该影响内部布局。
于是出现了我们今天熟悉的一系列"补丁式设计":
新机制 | 解决的问题 | 实际原理 |
---|---|---|
BFC(块级格式化上下文) | 隔离块间流动,防止 margin 合并、float 影响 | 强制创建独立块流容器 |
IFC(行内格式化上下文) | 让文字内的行盒独立排列 | 独立文字流 |
Float 浮动 | 让图片"脱离文流",文字环绕 | 元素退出 BFC 的垂直流 |
Position 定位 | 精确控制元素位置 | 完全脱离文档流 |
Flex / Grid | 明确化"独立容器布局" | 彻底从流式排版过渡到容器排版 |
🌱 BFC 就是 CSS 对"容器化需求"的第一次妥协。
它是"在文档流体系中,强制制造独立流"的 hack。
为什么不一开始就"容器化"?
这要从技术和哲学两方面理解。
技术限制
1990s 的浏览器,计算能力极弱。
如果每个盒子都独立计算布局上下文(类似今天的 flex/grid 子树),
那时的设备根本承受不了。
于是设计者选择了共享上下文、一次计算整页的模式:
- 性能高;
- 渲染快;
- 与文字流自然兼容。
排版哲学
当时的核心问题是:"让网页能像纸一样排版。"
所以,CSS 是为文本而生的,而不是为 UI 而生的。
共享上下文代表"自然连续",符合"段落式排版"的理念。
这正是为什么:
- margin 会塌陷(段落间距共享);
- 浮动能影响兄弟(图片与文字环绕);
- inline 元素的 vertical-align 只在文字行中起效。
这些行为,在"文档排版"语境下,是合理的。
只是在"界面布局"语境下,显得荒谬。
CSS 设计者后来怎么补救的?
CSS 的发展路径,就是从"文档式排版"到"容器式布局"的进化史:
阶段 | 特征 | 典型方案 |
---|---|---|
CSS1:文档流阶段 | 所有元素共享一个大流 | block, inline |
CSS2:浮动与定位阶段 | 增强控制力 | float, position |
CSS2.1:独立上下文阶段 | 支持隔离 | BFC, IFC |
CSS3:容器布局阶段 | 完全容器化 | flex, grid |
CSS4+:现代化阶段 | 子树级上下文、容器查询 | container query, subgrid |
从"纸面上的文字流" → "组件化的容器树",
CSS 的哲学发生了根本转变。
总结:这不是缺陷,而是历史遗留
我们遇到的"诡异问题" | 本质原因 | 历史背景 |
---|---|---|
margin 塌陷 | BFC 共享上下文 | 模仿段落间距 |
浮动破坏布局 | float 脱离垂直流 | 让图片环绕文字 |
vertical-align 无效 | 不在 IFC 中 | 僅对行内盒有效 |
clear 影响兄弟元素 | 流共享 | 源自文档流继承 |
这些"奇怪行为"都不是 bug,
而是因为 CSS 早期根本不是为现代界面布局而设计的。
从"流式排版"到"容器布局"的哲学转变
设计思想 | 典型机制 | 表现特征 |
---|---|---|
流式(Flow-based) | block / inline / float | 元素自然流动、共享上下文 |
容器式(Container-based) | flex / grid / container query | 元素独立计算、隔离上下文 |
现代 CSS(Flex、Grid)已经完成了这场哲学转型。
但那种"古老的流动思维",依旧深藏在 CSS 的根基里。
这就是为什么学习 CSS 时,
必须理解 文档流的历史起点 ------ 才能真正看懂它的"反直觉"。
CSS"流"体系三层结构------定位的模型基础
三层结构概念
概念 | 作用 | 对应现实中的比喻 | 范围 |
---|---|---|---|
文档流(Document Flow) | 决定元素在页面中的基础顺序与排列规则。 | 地面上铺开的"建筑规划线" | 整个文档级 |
格式化上下文(Formatting Context) | 控制子元素如何排布与影响彼此的规则环境 | 不同施工区域的"施工规则与地基分区" | 局部容器级 |
层叠上下文(Stacking Context) | 决定元素在z轴(前后)上的叠放关系,控制视觉层叠顺序的空间 | 同一块地上"建筑的垂直分层结构" | 视觉层级 |
所以:
- "脱离文档流" → 不再占据父元素中的正常流位置。
- "脱离格式化上下文" → 不再由创建BFC的盒子的布局规则约束。
- "脱离层叠上下文" → 不再受 z-index 的视觉层级影响。
这三个"上下文"是正交 的系统。
也就是说:脱离其中一个,不代表脱离其他。
层级关系图
plain
文档流 (Flow)
│
├─ 控制:盒子在页面的基本排列与占位
│
├─ 包含多个格式化上下文 (Formatting Context)
│ ├─ BFC (Block Formatting Context)
│ │ ├─ float layer
│ │ ├─ normal flow
│ │ └─ absolute layer
│ │ └─ 创建层叠上下文 (Stacking Context)
│ └─ IFC (Inline Formatting Context)
│
└─ 每个FC中,元素位置计算都依赖 含有块 (Containing Block)
├─ position / transform 等属性定义坐标系
└─ 仅控制几何关系,不影响流与层叠
历史脉络:从文档流 → BFC → 定位层 → 层叠上下文
1️⃣ CSS 诞生初期:只有"文档流"
最初的 CSS(CSS1 阶段)只有一种排版方式:
一切元素都按 普通文档流(Normal Flow) 自上而下排列。
- 没有浮动(float)
- 没有定位(position)
- 没有 z-index 层级
所有内容都是平面、顺序渲染。
2️⃣ CSS2:引入"BFC(块级格式化上下文)"
当引入了浮动 float、定位 position 后,
浏览器必须解决"哪些元素彼此影响、哪些互不干扰"的问题。
于是出现了:
BFC = Block Formatting Context,块级格式化上下文
它是排版层级的隔离机制。
BFC 解决的是 "空间布局"冲突 (例如 margin 折叠、浮动环绕等),
但仍然是"二维平面"的思维------没有"前后层级"。
3️⃣ 定位层的出现:absolute / fixed
随后,position: absolute | fixed
被引入。
浏览器需要区分:
- 谁"脱离文档流"?
- 谁"叠在上面"?
这就要求建立一个"空间维度(z轴)"的体系。
于是:
每个 BFC 内部,被进一步划分为几个"绘制层级(painting layers)":
plain
BFC
├── 背景层
├── 浮动层(float layer)
├── 常规流层(normal flow layer)
└── 绝对定位层(absolute positioning layer)
💡 这就是你前面提到的"定位层(absolute layer)"。
它是 BFC 内的一个子层 ,
代表"脱离常规流的、需要单独绘制顺序管理的元素"。
4️⃣ CSS2.1:引入"层叠上下文(Stacking Context)"
但仅有"定位层"还不够。
绝对定位元素之间,还会互相叠压(谁盖谁?谁透明?z-index 如何作用?)。
于是,W3C 提出了:
层叠上下文(Stacking Context) ------ 管理"z轴绘制顺序"的体系。
也就是说:
- BFC 管理二维空间(布局)
- Stacking Context 管理三维空间(视觉层次)
层叠上下文就是在 BFC 内的"绝对定位层"基础上,向"绘制与叠放控制"方向进化出来的一个体系。
定位(Position):对流的进一步控制
为什么会有定位?
浮动虽然能让元素"脱离文档流",但它依然受其他浮动、文字的影响,无法精确控制位置 。
于是 position
出现了。
包含块(Containing Block)
CSS规范原话(简化后)
根据 CSS2.1 §10.1:
The containing block for an element is formed by the nearest ancestor box that has a position other than static (i.e. relative, absolute, fixed, sticky).
也就是说:
- 当一个元素要计算它的
top
、left
、width: 50%
之类属性时,
它会往上查找最近的非 static 定位的祖先元素; - 这个祖先的padding box 区域,就成为它的包含块(Containing Block)。
为什么大家都说"relative"才行?
因为------
虽然"relative、absolute、fixed、sticky"都能创建包含块 ,
但在实际使用中:
position: relative;
- 不脱离文档流;
- 不改变盒子的层次结构;
- 对页面排版无副作用;
- 子绝对定位元素能直接参照它。
所以最安全、最常用。
这就导致------relative 成了"默认推荐"的创建包含块方式。
包含块的创建
属性 | 是否脱流 | 是否能成为包含块 | 常见误区 |
---|---|---|---|
relative |
否 | ✅ 能成为后代绝对定位的参照系 | ✅ 常用 |
absolute |
✅ | ✅ 能成为后代绝对定位的参照系 | ❌ 很少用于包裹别人 |
fixed |
✅ | ✅ 但参照的是视口(viewport) | ✅ 特殊用途(悬浮) |
sticky |
部分脱流(粘性) | ✅ 也能成为后代定位参考 | ❌ 很少被注意 |
static |
否 | ❌ 不创建包含块 | ✅ 默认状态 |
比如:
plain
<div class="a">
<div class="b">
<div class="c"></div>
</div>
</div>
<style>
.a { position: absolute; top: 50px; left: 50px; }
.b { position: absolute; top: 20px; left: 20px; }
.c { position: absolute; top: 10px; left: 10px; }
</style>
这里:
.c
的包含块不是.a
,而是最近的非 static 祖先.b
;.b
的包含块是.a
;.a
的包含块是 根元素(初始包含块)。
扩展:不仅仅是 position 能创建包含块
在现代 CSS 中,除了 position
相关属性,
还有其他属性也能隐式创建包含块,比如:
属性 | 说明 |
---|---|
transform |
任意非 none 值都会创建新包含块(同时创建层叠上下文) |
perspective |
同上 |
will-change: transform |
预先触发优化,也创建包含块 |
contain: layout 或 contain: paint |
创建独立的布局上下文与包含块 |
filter / backdrop-filter |
同样触发新的包含块与层叠上下文 |
所以,规范的表述是:
"包含块由一系列属性共同决定,并非仅由 position 控制。"
整体逻辑图(从属链)
plain
元素盒子(Element Box)
│
└── 包含块(Containing Block) ← 决定几何位置参照
│
├─ 最近的 position≠static 祖先
├─ 或 transform / contain / filter 等触发
└─ 若无,则使用初始包含块(整个视口或根元素)
结论总结
结论 | 解释 |
---|---|
❌ "只有 relative 才能创建包含块" 是不准确的。 | 它只是最安全、最常用的做法。 |
✅ "任何非 static 定位元素都可以成为包含块。" | relative / absolute / fixed / sticky 都行。 |
✅ "还有其他属性也能创建包含块。" | transform、contain、filter 等。 |
⚠️ "relative 不创建新的 BFC,但会创建新的包含块与潜在层叠上下文。" | 这点最容易混淆。 |
结尾
relative 只是"最温和的参照点创建者";
而 absolute、fixed、sticky、transform 等,
也都能创建包含块,只是它们"太强势",不适合拿来做温柔的参照。
五种定位模式对比表
定位模式 | 是否脱离文档流 | 是否建立新层叠上下文 | 是否创建包含块 | 说明 | 常见用途 |
---|---|---|---|---|---|
static(默认) | ❌ 否 | ❌ 否 | ❌ 否 | 完全受父级流支配,是文档流的最基础形态 | 默认普通布局 |
relative(相对定位) | ❌ 否(仍占位) | ⚠️ 否(除非设置 z-index) | ✅ 能成为后代绝对定位的参照系 | 视觉上"位移",但仍在文档流中 | 微调位置、不破坏布局 |
absolute(绝对定位) | ✅ 是 | ✅ 是(一定创建新层叠上下文) | ✅ 能成为后代绝对定位的参照系 | 从流中脱离,浮于父级 BFC 的绝对层上 | 精确摆放、弹窗、tooltip |
fixed(固定定位) | ✅ 是 | ✅ 是 | ✅ 但参照的是视口(viewport) | 坐标系固定于视口,不随滚动变化 | 导航栏、返回顶部按钮 |
sticky(粘性定位) | ❌ 否 | ⚠️ 否(仅当 z-index 触发时) | ✅ 也能成为后代定位参考 | 在"相对定位"和"固定定位"之间动态切换 | 顶部吸附导航 |
定位后脱离了文档流,那么是否脱离了BFC?
定位元素的真正位置:脱流但仍属 BFC 管辖
绝对定位元素确实"脱离文档流"
当我们给元素加上:
plain
position: absolute;
它就不会在父级的常规流中占位 。
也就是说,它不再参与"块盒的垂直排列"或"行内文字的水平排列"。
举例:
plain
<div class="box">
<p>文字A</p>
<div class="abs"></div>
<p>文字B</p>
</div>
即使 .abs
写在中间,它也不会在"文字A"和"文字B"之间占空间。
但它仍"隶属于"某个 BFC ------ 在其中的"绝对定位层"中绘制
即使脱离文档流,CSS 仍需要知道:
- 绝对定位元素的 包含块(containing block);
- 它要相对于谁去定位;
- 它要在哪个"绘制层级"出现。
这些都需要一个"逻辑归属环境"------那就是 BFC。
所以,每个 BFC 内部,CSS 引擎都会维护三层绘制层级(来自 CSS2.1 §9.9):
plain
BFC
├── 浮动层(float layer) ← 浮动元素
├── 常规流层(normal flow layer) ← 普通文档流内容
└── 绝对定位层(absolute layer) ← 绝对定位内容
💡换句话说:
BFC 就像一个大舞台,演员有三类:
- 正常演员:站在原地(常规流)
- 飘起来的演员:浮动在一侧(浮动层)
- 飞出舞台但仍挂着安全绳的演员:绝对定位层
它们都属于同一个舞台(BFC),只是站在不同的"楼层"。
BFC 为什么要包含"绝对定位层"?
因为BFC 是绘制的基本单位 ,
浏览器渲染树(Render Tree)在计算绘制时,不仅计算流,还要确定:
- 每个元素的 坐标系;
- 绘制顺序(float → normal → absolute);
- z-index 层叠。
而绝对定位元素的"坐标系"就来源于:
它所在 BFC 的 包含块(containing block)。
所以它必须被 BFC 纳入逻辑管理。
类比形象:BFC 像是一座三层大厦
我们可以这样想象整个机制:
plain
┌────────────────────────────┐
│ BFC(块级格式化上下文) │
│ ┌──────────────────────┐ │
│ │ 浮动层(float layer) │ ← 浮动的元素漂在上方
│ ├──────────────────────┤ │
│ │ 常规流层(normal flow) │ ← 块与文字正常排列
│ ├──────────────────────┤ │
│ │ 绝对定位层(absolute) │ ← 脱流元素悬空绘制
│ └──────────────────────┘ │
└────────────────────────────┘
这三层虽然"视觉上"分离,但在"结构上"同属一个 BFC。
所谓的"脱离文档流"只是脱离"占位规则" ,
而非脱离布局环境(BFC)。
绝对定位的元素,就像"飞起来但仍拴在绳子上的风筝",
那根绳子------正是它所在的 BFC。
问题补充
问题 | 结论 |
---|---|
为什么说绝对定位会脱离文档流? | 因为它不再占据常规流位置,不影响兄弟布局。 |
那为什么仍属于 BFC? | 因为它的定位坐标、绘制顺序、层叠关系仍依赖 BFC 的上下文。 |
BFC 中的"绝对定位层"存在意义? | 用于管理该上下文下的所有绝对定位子元素的绘制与坐标系。 |
是否可以不属于任何 BFC? | 仅当是 position: fixed 或 position: sticky 时,才由 viewport 或滚动容器建立独立坐标系。 |
逐个深入分析
position: static
------ 文档流的"默认秩序"
plain
div { position: static; }
- 元素参与正常的块/行内排版;
- 不脱离文档流;
- 不创建任何上下文;
- 仅依附父级 BFC 的规则。
类比:它就是一个听话的士兵,
按父级的"流动规则"在自己的格子里排好队。
position: relative
------ 不脱流的"视觉位移"
plain
div { position: relative; top: 10px; left: 20px; }
- 仍参与文档流,原有空间保留;
- 在绘制阶段向指定方向偏移;
- 不会创建新的 BFC;
- 但会创建一个"新的包含块(Containing Block)";
- 如果加上
z-index
,会触发新的层叠上下文。
类比:
它站在原地没动(仍在队伍里),
只是身体往旁边挪了一点;
并且举起了一面旗子,告诉后代:
"绝对定位的孩子们,以我为坐标。"
所以:position: relative; 会为绝对定位子元素开启新的绝对定位坐标系",但它不会创建新的 BFC,只是在现有 BFC 中定义了一个局部坐标基准。
position: absolute
------ 脱流的"浮空盒"
plain
div { position: absolute; top: 0; left: 0; }
- 完全脱离文档流;
- 不在父 BFC 的"常规层"中出现;
- 被绘制在父 BFC 的 "绝对定位层(absolute layer)";
- 会创建 新的层叠上下文(Stacking Context);
- 其位置以最近的
positioned ancestor
(relative/absolute/fixed/sticky) 为基准。
类比:
它从队伍里飞起来了,
但仍用绳子拴在最近那个"positioned"祖先身上。
绘制时仍归属于该祖先的 BFC 舞台。
position: fixed
------ 相对于视口的"顶层脱流"
plain
div { position: fixed; bottom: 0; right: 0; }
- 完全脱离文档流;
- 不再属于任何父级 BFC;
- 自己形成新的 BFC;
- 同时形成新的层叠上下文;
- 坐标参考视口(或新的独立渲染上下文(Compositing Layer))。
类比:
它已经不再属于舞台上的任何队伍,
而是直接钉在了"摄影机的屏幕上"。
触发相对滚动容器定位的情况
- 祖先设置了:
transform
perspective
filter
backdrop-filter
contain: paint
will-change: transform
等属性时。
这些属性的效果是:让该元素成为一个新的图层(layer),从而影响 fixed 的定位基准。
例演示:
html
<div class="scroll-box">
<div class="fixed-child">I'm fixed?</div>
<div style="height:2000px;"></div>
</div>
css
.scroll-box {
width: 300px;
height: 200px;
overflow: auto;
transform: translateZ(0); /* 注意这一句!触发复合层 */
background: #f5f5f5;
border: 2px solid #ccc;
}
.fixed-child {
position: fixed;
top: 0;
left: 0;
background: orange;
padding: 10px;
}
👉 结果:
- 按理说
position: fixed
应该固定在浏览器视口的左上角; - 但因为父元素
.scroll-box
有transform: translateZ(0)
; - 浏览器会认为
.scroll-box
是一个新的"视口"; - 所以
.fixed-child
固定在容器的左上角; - 当
.scroll-box
滚动时,它会跟着一起动。
🔹直观理解:
"fixed 固定在视口"只是默认行为,当父级成为独立渲染上下文(Compositing Layer)时,它就被"困"在该父级里了。
position: sticky
------ 相对与固定之间的"混合体"
plain
div { position: sticky; top: 10px; }
- 默认表现为相对定位(在流中);
- 当滚动到一定阈值时,切换为固定定位(脱流);
- 不创建新的 BFC;
- 但可在激活阶段(若有 z-index)生成层叠上下文;
- 坐标系仍由最近的可滚动祖先决定。
类比:
它原本在队伍里行走,
当队伍向上滚动时,它被"磁吸"住,暂时固定在视口上。
当滚动过去后,又回到队伍里继续走。
设计初衷
传统定位有两个极端:
relative
→ 相对父元素偏移,但会随文档滚动;fixed
→ 固定在视口,不随滚动。
于是 CSS 想出一个折中方案:
"当我在容器内还没滚到某个阈值时,按 normal flow 布局;
一旦滚到阈值,就暂时固定在容器内。"
定义与机制
plain
position: sticky;
top: 0; /* 当元素顶部到容器顶部0px时固定 */
关键点:
- sticky 不脱离文档流,在正常流中保留空间;
- 相对于最近的可滚动祖先(overflow:auto/scroll)或视口生效;
- 只有在**到达阈值(top/bottom/left/right)**后才"粘住";
- 超出祖先容器边界时,会被容器裁剪。
示例代码
html
<div class="container">
<div class="header">Header</div>
<div class="content">
<div class="sticky">I'm sticky</div>
<p>... lots of content ...</p>
</div>
</div>
css
.container {
width: 400px;
height: 300px;
overflow: auto;
border: 2px solid #333;
}
.header {
height: 60px;
background: #999;
}
.content {
height: 800px;
background: #eee;
}
.sticky {
position: sticky;
top: 0;
background: orange;
padding: 10px;
}
👉 结果说明:
- 当
.content
向上滚动时,.sticky
元素会"贴在"滚动容器.container
的顶部; - 继续滚动到
.container
底部时,它会被容器边缘"带走"; - 它只在容器的可视范围内固定。
模式对比
特性 | absolute |
fixed |
sticky |
---|---|---|---|
参考系 | 最近的包含块 | 视口或 transform 容器 | 最近的滚动容器 |
是否脱流 | ✅ 是 | ✅ 是 | ❌ 否 |
是否创建层叠上下文 | ✅ | ✅ | 有 z-index 时 |
是否参与文档流 | ❌ | ❌ | ✅ |
是否被滚动带动 | ✅(跟随滚动容器) | ❌(但可例外) | 部分(阈值前滚动,阈值后固定) |
模式 | 比喻 |
---|---|
absolute |
"我自由浮动在父级的地图上。" |
fixed |
"我贴在屏幕上,除非被放进另一个盒子(transform)。" |
sticky |
"我在盒子里滑动,滑到顶后,暂时粘住,等你滚远了再走。" |
为什么 sticky 明明参与文档流,却能"固定"在顶部?
乍一看矛盾:
- 文档流中的盒子不是会随父容器一起滚动吗?
- 为什么滚到某个位置 sticky 却突然"停住"了?
要理解这一点,必须先认识两套不同但叠加的机制:
- 布局阶段(layout):决定盒子"应当"出现在文档中的位置。
- 绘制与合成阶段(paint + compositing):决定盒子"实际"在屏幕上的位置。
sticky 的底层机制:布局 + 视口的"双重判断"
当浏览器计算 sticky 元素的位置时,会经历这样几个阶段:
1️⃣ 布局阶段
- sticky 元素最初按照普通文档流(normal flow)参与排版;
- 浏览器记录它在容器内的"静态位置"。
2️⃣ 滚动阶段(scroll event + paint 阶段)
- 每当容器滚动,浏览器重新判断:
该 sticky 元素的顶部距离容器的顶部是否小于 top
指定的阈值?
- 如果还没滚到阈值 → 保持普通文档流;
- 如果已经超过阈值 → 将该元素在合成阶段提取出来,贴在容器的合成层上 ,并冻结其
top
距离; - 但注意:在布局树(layout tree)中,它的位置仍然被保留(即仍"占位")。
3️⃣ 滚动超过容器底部时
- sticky 元素会再次"释放",回到普通流动;
- 这是因为 sticky 的固定范围只能在包含块的边界内。
通俗比喻:
sticky 就像一个在滚动箱子里的便利贴:
- 它最初贴在内容上一起滑;
- 当内容滑到顶,它被卡在盖子上(top=0);
- 当继续往上滚时,便利贴会被盖子带出视野;
- 整个过程中,它在纸上的位置始终保留(即"参与文档流")。
transform
transform
是一个视觉变换属性,用于改变元素在二维或三维空间中的几何形态,比如:
plain
transform: translate(20px, 30px);
transform: scale(1.2);
transform: rotate(45deg);
但重要的是:
一旦元素被应用 transform,浏览器必须将它的绘制单独提取出来 ,
以便独立计算位置与透明度(这叫做"合成层 compositing layer")。
transform 做了两件关键的事:
- 创建新的包含块(Containing Block)
- 所有其子元素(尤其 absolute、fixed)将以它为定位参考;
- 因此 "fixed 不再参考视口" 的情况通常是 transform 导致的。
- 创建新的图层(compositing layer)
- 该层可以在 GPU 上独立渲染与合成;
- 这也是为什么 transform 动画通常比 left/top 动画更流畅。
代码案例
plain
<div class="outer">
<div class="inner">I'm fixed?</div>
</div>
plain
.outer {
height: 300px;
overflow: auto;
transform: translateZ(0); /* 创建独立合成层与包含块 */
}
.inner {
position: fixed;
top: 0;
left: 0;
background: orange;
}
结果:
- 本应相对视口固定;
- 但因为
.outer
创建了一个新合成层; - 所以
.inner
被"困"在.outer
的坐标系中; - 当
.outer
滚动时,它也会跟着动。
transform 与 BFC、文档流、视口的关系
概念 | 层级类型 | 职责 | 与 transform 的关系 |
---|---|---|---|
文档流 (Document Flow) | 结构层 | 决定盒子上下左右的排版关系 | transform 不改变流关系(不脱流) |
格式化上下文 (BFC/IFC) | 排版层 | 控制内部元素如何排列 | transform 不影响 BFC 流式规则 |
包含块 (Containing Block) | 坐标层 | 决定绝对/固定定位元素的参照系 | transform 会创建新的包含块 |
层叠上下文 (Stacking Context) | 视觉层 | 控制 z 轴上的绘制顺序 | transform 自动创建新的层叠上下文 |
合成层 (Compositing Layer) | GPU 层 | 控制最终在屏幕上的位置与重绘性能 | transform 会触发独立合成层 |
整体总结(形象化)
plain
文档流(树干) ------ 定义结构位置
│
├─ 格式化上下文(树枝) ------ 定义排版规则(块或行内)
│
├─ 包含块(坐标系) ------ 定义定位参考点
│
├─ 层叠上下文(Z轴) ------ 定义谁在谁上面
│
└─ 合成层(GPU图层) ------ 定义最终绘制在哪儿、是否独立重绘
sticky
之所以能粘,是在合成层阶段修改绘制位置;transform
之所以能"困住 fixed",是因为它生成了新的坐标系与图层;- BFC、IFC 是排版规则;
包含块是坐标体系;
层叠上下文是绘制顺序;
合成层是最终的"显示平面"。
副作用
定位让元素脱离流,会带来:
- 层叠问题(需要
z-index
管理); - 父元素高度塌陷(无论是否开启BFC);
- 无法自然响应布局(不随窗口变化自动流动)。
层叠(Stacking)
层叠上下文(Stacking Context)
属性设置 | 是否创建层叠上下文? | 是否脱离文档流? | 是否创建包含块? |
---|---|---|---|
position: static |
否 | 否 | 否 |
position: relative |
仅当设置 z-index 时创建层叠上下文 |
否 | ✅ 创建包含块 |
position: absolute |
✅ 无论 z-index 是否声明 |
✅ 脱离文档流 | ✅ 创建包含块 |
position: fixed |
✅ 无论 z-index 是否声明 |
✅ 脱离文档流 | ✅ 创建包含块(相对视口) |
position: sticky |
当设置 z-index 时创建 |
否 | ✅ 创建包含块 |
案例:如果一个盒子直接设置 z-index=999,那么它会被定位到哪里?
CSS 渲染的"三阶段"流程
浏览器绘制一个页面时,大体分三步:
阶段 | 做什么 | 结果 |
---|---|---|
① 布局(Layout) | 确定每个盒子的几何空间(宽高、位置) | 文档流 / BFC / IFC |
② 分层(Layering) | 将不同类型的内容分到不同的绘制层 | 背景层、浮动层、定位层... |
③ 绘制与层叠(Painting & Stacking) | 根据层叠上下文与 z-index 进行绘制排序 | 层叠上下文树 |
先说前提:z-index 必须"依附"于一个层叠上下文
什么是层叠上下文?
层叠上下文(Stacking Context, SC)是一个 局部的三维绘制空间 。
里面的所有元素,按照一套固定的层叠规则(z-index、position、opacity等)
相对地进行叠放。
通俗比喻:
整个页面是一个大画布(根层叠上下文),
每个层叠上下文都是一张"独立透明胶片",
胶片内部先画完再压到主画布上。
如果你写了 z-index: 999;
,浏览器的思考顺序是这样的
步骤 1️⃣:检查这个盒子是否"有资格使用 z-index"
浏览器会看它的 position 属性:
情况 | 结果 |
---|---|
position: static(默认) | 无效!忽略 z-index |
position: relative / absolute / fixed / sticky | 合法,可以参与层叠 |
其它触发 SC 的情况(如 opacity<1, transform 等) | 也合法,创建新的层叠上下文 |
也就是说:
z-index 想生效,前提是元素属于某个层叠上下文(自己创建的或继承的)。
步骤 2️⃣:确定当前所属层叠上下文
浏览器从下往上找,最近的层叠上下文是谁:
- 如果父级是
position: relative; z-index:10;
→ 那么当前盒子就在这个父级的层叠上下文里。 - 如果一直找不到,
→ 那么属于根层叠上下文 (通常是<html>
)。
这一步完成了归属。
步骤 3️⃣:确定在该层叠上下文的哪一层(stacking level)
每个层叠上下文内部有七个固定的"绘制层次"(层叠等级,W3C 定义):
层叠顺序(从底到顶) | 元素类型 |
---|---|
1️⃣ 层叠上下文的背景与边框 | |
2️⃣ 负 z-index 的子层叠上下文 | |
3️⃣ 普通文档流内容(非定位元素) | |
4️⃣ 浮动元素 | |
5️⃣ 行内块、inline-block、inline-table | |
6️⃣ z-index:auto 的定位元素 | |
7️⃣ 正 z-index 的定位元素(z-index: 1, 999 ...) |
所以当你写下:
plain
.child {
position: relative;
z-index: 999;
}
浏览器会将它放入当前层叠上下文的 第7层:正 z-index 层 。
而 999
只是用于在同一层的元素之间再进一步排序(按数值大小叠放)。
步骤 4️⃣:确定层叠上下文之间的关系
每个层叠上下文内部排序完成后 ,
浏览器再把所有层叠上下文按照父子嵌套关系叠起来。
父层叠上下文中的一个盒子,
即使 z-index=999999,
也不会盖过父层叠上下文外的元素。
(因为层叠上下文是封闭的空间。)
步骤 5️⃣:进入绘制阶段
最后浏览器进入绘制(painting):
- 绘制当前层叠上下文的背景;
- 绘制普通流、浮动、定位元素;
- 绘制子层叠上下文;
- 按 z-index 顺序从小到大层叠。
于是,你的 z-index:999
元素出现在:
- 当前层叠上下文的最上层;
- 但不会越过外层层叠上下文。
结合 "BFC 层级结构" 的可视化关系
我们重新把两者叠在一起看:
plain
BFC(块级格式化上下文)
│
├── 背景层(background layer)
│
├── 浮动层(float layer)
│
├── 常规流层(normal flow layer)
│ └── 普通块与文字
│
└── 绝对定位层(absolute positioning layer)
├── 定位元素(relative/absolute/fixed/sticky)
└── 层叠上下文(SC)
├── 背景(z-index: auto)
├── 负 z-index 元素
├── 普通内容
├── 正 z-index 元素 ← 你的 z-index:999 在这里
└── 子层叠上下文(递归)
可以看到:
- BFC 决定盒子的"摆放位置(几何空间)";
- 层叠上下文(SC) 决定盒子的"叠放顺序(绘制空间)"。
你的 z-index:999
是在 BFC 的绘制阶段 被放入 SC 的顶层。
一个完整的渲染故事举例
plain
<div class="parent">
<div class="child"></div>
</div>
plain
.parent {
position: relative; /* 创建包含块 */
z-index: 1; /* 创建层叠上下文(SC#1) */
}
.child {
position: absolute; /* 进入 .parent 的定位层 */
z-index: 999; /* 在 SC#1 内的最上层 */
}
渲染顺序:
1️⃣ 布局阶段:
.parent
参与外层 BFC,.child
脱离常规流。
2️⃣ 创建 SC#1:
.parent
是层叠上下文的根。
3️⃣ .child
在 SC#1 中的正 z-index 层(topmost)。
4️⃣ 绘制阶段:
浏览器先画 .parent
的背景,再画 .child
,
但这两者都在 SC#1 内,不会压过其它 SC。
总结
概念 | 作用 | 与 z-index 的关系 |
---|---|---|
BFC | 布局二维平面 | 决定盒子在 X/Y 上的几何位置 |
定位层 | BFC 内的子层 | 容纳脱流元素 |
层叠上下文(SC) | 三维绘制空间 | 决定 z 轴上的叠放顺序 |
z-index | SC 内部的排序号 | 只有在 SC 中才生效 |
z-index: 999
不是"把元素提到全世界最上面",
而是"在当前层叠上下文的顶层"画出来。
总结浮动与定位
浮动与定位的时代地位
时代 | 主流布局方式 | 关键技术 | 特点 |
---|---|---|---|
1990s | 文档流布局 | block + inline | 纯流式,适合文本 |
2000s | 浮动布局 | float + clear | 多栏布局的时代 |
2010s | 定位布局 | position 系列 | 精确控制空间 |
2015+ | 弹性布局 | flexbox | 一维自适应布局 |
2020+ | 网格布局 | grid | 二维布局体系 |
浮动与定位的本质区别
对比项 | Float | Position |
---|---|---|
初衷 | 文字环绕 | 精确定位 |
是否脱离文档流 | 是(部分) | 是(完全) |
是否仍参与BFC | 是 | 否(绝对/固定) |
是否影响兄弟元素 | 是(避让) | 否 |
是否建立新层叠上下文 | 否 | 是(非 static 才建立) |
是否可与流共存 | 可以(float+text) | 不可以(absolute/fixed 脱流) |
浮动与定位是"控制流的两种方式"
- 文档流 = 默认规则(自然排版)
- 浮动 = 局部漂浮(部分脱流)
- 定位 = 精确锚定(完全脱流)
它们是人类从"顺流而下"到"逆流掌控"的关键进化节点。
后来的 flex、grid,本质上是"在不脱流的情况下,更灵活地控制流动方向与空间分配"。