CSS排版布局篇(4):浮动(float)、定位(position) 、层叠(Stacking)

浮动(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).

也就是说:

  • 当一个元素要计算它的 topleftwidth: 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: layoutcontain: 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: fixedposition: 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-boxtransform: 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时固定 */

关键点:

  1. sticky 不脱离文档流,在正常流中保留空间;
  2. 相对于最近的可滚动祖先(overflow:auto/scroll)或视口生效;
  3. 只有在**到达阈值(top/bottom/left/right)**后才"粘住";
  4. 超出祖先容器边界时,会被容器裁剪

示例代码
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 却突然"停住"了?

要理解这一点,必须先认识两套不同但叠加的机制:

  1. 布局阶段(layout):决定盒子"应当"出现在文档中的位置。
  2. 绘制与合成阶段(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 做了两件关键的事:
  1. 创建新的包含块(Containing Block)
    • 所有其子元素(尤其 absolute、fixed)将以它为定位参考;
    • 因此 "fixed 不再参考视口" 的情况通常是 transform 导致的。
  2. 创建新的图层(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):

  1. 绘制当前层叠上下文的背景;
  2. 绘制普通流、浮动、定位元素;
  3. 绘制子层叠上下文;
  4. 按 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,本质上是"在不脱流的情况下,更灵活地控制流动方向与空间分配"。

相关推荐
昭昭日月明2 小时前
mac 效率工具:Raycast 的扩展开发
前端·mac·设计
white-persist2 小时前
XXE 注入漏洞全解析:从原理到实战
开发语言·前端·网络·安全·web安全·网络安全·信息可视化
练习时长一年3 小时前
Spring内置功能
java·前端·spring
SHUIPING_YANG3 小时前
完美迁移:将 nvm 和 npm 完全安装到 Windows D 盘
前端·windows·npm
lypzcgf3 小时前
Coze源码分析-资源库-编辑数据库-前端源码-核心组件
前端·数据库·源码分析·coze·coze源码分析·ai应用平台·agent平台
勤奋菲菲3 小时前
Koa.js 完全指南:下一代 Node.js Web 框架
前端·javascript·node.js
晒太阳5794 小时前
懒加载与按需加载
前端
10年前端老司机4 小时前
面试官爱问的 Object.defineProperty,90%的人倒在这些细节上!
前端·javascript