开始根据林想的Web工坊大佬的建议路线学习CSS。
本文涉及内容会如下:
CSS positioned layout - CSS | MDN
Block and inline layout in normal flow(正常流中的块级与行内布局)
CSS 2.1 规范定义了正常流(normal flow)的默认行为:所有盒子都属于某个格式化上下文(formatting context),并且只能是 block 或 inline 其中之一,不可能同时两者。
Block 格式化上下文的行为规则
在 block formatting context 中,盒子一个接一个地垂直排列,从容纳块的顶部开始。相邻兄弟盒子之间的垂直距离由 margin 决定,并且会发生我们所熟知的 margin 折叠(collapse)。每个盒子的左外边缘贴着容纳块的左边缘——哪怕盒子浮动之后也是如此(RTL 语言中则是贴着右边缘)。
Block 元素还有一个重要的默认行为:它们会占据容纳块在 inline 方向上的全部空间,也就是宽度的 100%。就算你给 block 元素设了一个固定宽度,它们依然是垂直堆叠的,不会并排——想让 block 元素并排,你得借助 float、flexbox、grid 这些机制。
Inline 格式化上下文的行为规则
Inline 格式化上下文的行为跟 block 完全不同:它让盒子一个接一个地水平排列,从容纳块顶部开始。水平方向上的 margin、border、padding 都能正常生效,把元素之间左右推开;垂直对齐方式也多种多样——底部对齐、顶部对齐,或者以文字基线为基准。包裹一行盒子的矩形区域叫做行盒(line box)。
当 inline 的内容在一行放不下时,浏览器会自动换到下一行继续排列。每行的高度由该行中最高的那个盒子决定。
这里有一个容易被忽略的细节:像 <strong> 这种 inline 元素前后的纯文字,浏览器会为之自动创建匿名盒子(anonymous box)来包裹。为什么需要它们?
回到开头,CSS 2.1 规范有一条铁律,block 容器内的所有内容必须被组织成 block 盒子或 inline 盒子的序列,不允许”裸文本”参与布局。两边的纯文字没有 DOM 元素来生成盒子,浏览器只能在布局阶段强制为它们创建匿名盒子来承载1。
那为什么 CSS 选不中它们?因为匿名盒子只存在于渲染流水线的布局树(layout tree)中(在未最终落定时,匿名盒子在渲染树(render tree / box tree)中,同一个棵树的不同时段),不在 DOM 树上。CSS 选择器的命中目标始终是 DOM 元素节点,而匿名盒子的起点根本不是 DOM 元素,没有选择器能抵达它。使用者可以”感觉到”它的存在(比如行高、继承行为),但摸不到。
display 属性与流布局的关系
display 属性的值直接决定一个元素进入哪种格式化上下文:display: block 表示这个元素要生成 block 级别的盒子、走 block 布局;display: inline 则生成 inline 级别的盒子、走 inline 布局;display: inline-block 是一种有趣的混合——对外表现像 inline 元素(不换行),但内部走的是 block 布局(可以设宽高);而 display: none 意味着元素根本不会在格式化树中生成任何盒子,相当于从布局中彻底消失。
Introduction to formatting contexts(格式化上下文入门)
如果说 block/inline 是单个盒子的行为规则,那么格式化上下文(formatting context)就是规定了”怎样在这些规则下摆放一群盒子”的区域。页面上的每一个元素都属于某个格式化上下文——不存在”没有上下文”的元素。最常见的四种:
| 上下文类型 | 子元素布局规则 |
|---|---|
| Block Formatting Context (BFC) | 按 block 规则垂直排列 |
| Inline Formatting Context (IFC) | 按 inline 规则水平排列 |
| Flex Formatting Context | 按 flexbox 规则排布 |
| Grid Formatting Context | 按 grid 规则排布 |
<html> 根元素创建了页面上第一个、也是最初始的BFC。可以把 BFC 理解为一个结界:BFC 内部发生的任何事情都不会泄漏到外部去,就像 <html> 元素隔开了画布和文档整体一样。
如何创建新的 BFC
除了根元素天生的 BFC 之外,你也可以给任意元素新建 BFC。MDN 列出了若干触发条件,常见的有这些:
float不为noneposition: absolute或fixeddisplay: inline-blockoverflow不为visible(如overflow: hidden、overflow: auto)- ⭐
display: flow-root——现代推荐方式 contain: layout、content或strict- flex items 和 grid items 自身也会新建 BFC
display: flow-root是最干净的方案:它唯一的作用就是创建 BFC,没有任何overflow: hidden那样的副作用(比如无意中裁掉内容或生成滚动条)。flow-root这个名字的语义就是”像根元素<html>一样创建一个 BFC”。
BFC 到底有什么用
面试经常问 BFC,不是因为它的创建条件能背出来,而是因为它在三个实际场景中不可替代。
包含浮动(contain floats)。当一个父容器里有 float 子元素时,子元素脱离正常流,父容器的高度会塌陷到零,因为父容器不知道浮动的子元素有多高。但如果让父容器变成一个 BFC,它就会”看到”内部的所有浮动子元素,高度不再塌陷。
阻止外边距折叠(prevent margin collapsing)。之前的 cssday3 讲过,相邻 block 元素的垂直 margin 会折叠。但不同 BFC 之间的 margin 不会折叠。给父容器新建一个 BFC,就相当于筑了一道墙,子元素的 margin 不会再穿透出去跟外部的 margin 打架。
清除浮动需要同一个 BFC。clear 属性只在同一个 BFC 内部生效。不同 BFC 的元素之间 clear 管不着。
BFC 的三个用途串起来会发现:float 诞生时只是一个”文字环绕”的补丁,后续的 clear 和 BFC 都是在补丁之上继续打补丁:clear 让后续元素停止环绕,BFC 让父容器重新包含 float。整个体系是历史的产物,不是设计的结果。
display: flow-root:史上第一个”专职” BFC 创建方式
在这个补丁链上,还有一个更细节的问题:创建 BFC 本身也需要打补丁。在 flow-root 出现之前,所有创建 BFC 的方法都是拿别的属性的副作用凑合:
| 方式 | 它本来是来干嘛的 | BFC 是副作用 |
|---|---|---|
overflow: hidden | 裁掉溢出内容 | 顺便创建了 BFC → 内容真溢出你也看不见了 |
overflow: auto | 溢出时给滚动条 | 顺便创建了 BFC → 不溢出时也可能冒滚动条 |
float: left | 让元素飘起来给人绕 | 顺便创建了 BFC → 父元素自己也飘了 |
display: inline-block | 变成行内块 | 顺便创建了 BFC → 对外表现得像 inline |
position: absolute | 脱离正常流定位 | 顺便创建了 BFC → 整个元素翻出正常流 |
开发者只是想”让父容器包含浮动”,却要被迫接受裁切、滚动条、或者定位方式改变。所以 CSS 规范终于给 BFC 一个专门的属性:
1
2
3
4
5
/* 旧方案:副作用不可控 */
.container-old { overflow: auto; }
/* 官方方案:只做一件事,就是创建 BFC,其他什么都不干 */
.container-new { display: flow-root; }
flow-root 是第一个专门为”创建 BFC”而生的属性值。名字本身就是描述:”在这里建立一个流的根节点”,就像 <html> 根元素天生就是一个 BFC。没有裁切、没有滚动条、不改变定位、对外部布局零影响。
但这是补丁干净化,不是布局革命化。flow-root 让 float + clear + BFC 这条补丁链更好用了,但不改变它本质上是补丁的事实。 真正的解决方案是 Flexbox 和 Grid:display: flex 一行搞定并排布局,不需要 float、不需要 clear、不会塌陷、不需要 BFC。这些概念在下一次技术迭代里直接被设计掉了。
关于浮动(float)的补充:上面反复提到了 float、包含浮动、清除浮动,如果对 float 本身还不熟悉,这里有一个从零开始的解释。
浮动是什么?
float 是 CSS 最老牌的布局手段之一,比 Flexbox 早了十几年。它的设计初衷非常简单:让图片被文字环绕,像报纸排版一样。
1
2
3
4
5
6
<p>
<img src="photo.jpg" style="float: left; width: 120px; height: 80px">
这是一段很长的文字,它会环绕在图片的右侧,像报纸排版一样,
文字自动填充图片旁边的空白。当文字长到超过图片高度时,
后续的文字会回到左边缘,填满整行。
</p>
效果:
1
2
3
4
5
┌────────┐ 文字文字文字文字文字文字
│ 图片 │ 文字文字文字文字文字文字
│ 80px │ 文字文字文字文字文字文字
└────────┘ 文字文字文字文字文字文字 ← 文字超过图片高度,自动回到左边缘
文字文字文字文字文字文字文字文字文字 ← 填满整行,形成"环绕包围"效果
float: left 做了两件事:图片脱离正常流(飘起来了),但后面的文字行盒会缩短,自动绕开它。关键点:环绕是 float 自带的效果,不需要任何额外操作。
为什么需要清除浮动?
当你不想让某些后续内容继续绕行时。比如下一个段落应该从图片下方重新开始:
1
2
3
4
5
<div>
<img src="a.jpg" style="float: left">
<p>这段文字绕在图片右边,没问题</p>
</div>
<p style="clear: both">这段文字我不想绕!强制换到图片下方。</p>
clear: both 的作用是停止环绕,告诉浏览器”两侧的浮动元素都别再影响我了”。它不是开启环绕,而是刹车。
浮动为什么不能用 block 和 inline 来做?
也许会有这样的问题:把图片和文字各放一个盒子,不是更自然吗?比如图片放一个 div,下面放文字 p,它们之间有 margin,整齐的上下排列。但上下排列意味着文字永远到不了图片旁边,共享空间的效果做不出来。那不用盒子,让图片和文字都 inline 自然同行呢?也不行。inline 流里图片被当作一个巨大的字符,一整行排完就换行,图片下面的空白区域文字进不去,环绕效果同样做不出来。
这是 CSS 历史上的一个根本缺陷:block/inline 的排版模型诞生于 1996 年,它参考的是纯文本文档,段落堆叠、标题加粗,够用。但”报纸排版”,图片嵌在文字中间、文字自然绕排、图片下方空间也能被文字利用,这个需求 block/inline 做不了。Inine 的规则是”每一行排满换行”,图片占整个行盒的高度,下面的空间空着也得空着:
1
2
3
4
5
6
┌────────┐ 文字文字文字文字文字
│ 图片 │ ← 这一行排满就结束了
│ 高 │ ← 图片下面空着,inline 文字进不来
│ │
└────────┘
文字文字文字文字文字文字文字 ← 只能在图片下面重新起一行
于是 CSS 打了一个补丁:不重新设计排版模型,而是在已有的 block/inline 模型上加一个例外规则,也就是float。它的逻辑是:元素飘出正常流,但后面的文字行盒在计算宽度时把 float 占的空间剪掉,文字自动绕过。float 这个名字本身就是”悬浮在正常流之上,但影子还在影响文字排列”。
因为 float 是补丁而不是重新设计的布局系统,清除浮动才这么麻烦。开发者需要在补丁之上再打补丁(clear),然后发现补丁又有副作用(父容器高度塌陷),于是再引入 BFC 来擦屁股。
这才是后来 Flexbox 和 Grid 被发明的根本原因:不是给例外规则,而是基于开发者一个全新的布局环境,在里面所有元素自然共享空间,不需要 float、不需要 clear、不需要 BFC。
Inline 格式化上下文(IFC)
IFC 不像 BFC 那样有独立的”结界”,它通常存在于其他格式化上下文内部——你可以把它理解为一段文字的段落级布局上下文。
IFC 中有一个反直觉的行为:垂直方向的 margin 完全不生效——不是折叠,是压根就不存在。但水平方向的 margin、padding、border 都能正常推开左右文字。另外,垂直方向的 padding 和 border 虽然会渲染,但可能重叠到上下内容上去,因为行盒(line box)不会因为 padding 或 border 而增加自己的高度。换句话说,盒模型在 IFC 里并不完全适用——只有水平维度的间距是可靠的。
In flow and out of flow(在流内与脱离流)
正常情况下,所有元素都在文档流里(in flow),按 HTML 中出现的顺序逐一排列。但某些布局机制会把元素从正常流中”拽出来”,让它们脱离流的约束。
对于流的定义,如果要简要地比喻,那么流是水,BFC 和 IFC 则是两个不同的河道。
水永远在某个河道里走,开发者改变
display、float、position,本质上是在切换或创建新的河道。而Flexbox 和 Grid 是第三代河道,比 BFC/IFC 更符合直觉。
三种脱离流的方式
float 是最早的脱离流方式之一。一个 float 元素会先按正常流规则计算自己的起始位置,然后脱离流,尽量向左或向右移动。有趣的是,虽然 float 元素本身的盒子脱离了流(后续 in-flow 元素会当它不存在),但文字不会覆盖它——后续段落的行盒(line box)会自动缩短,形成文字环绕 float 的效果。不过段落的盒子本身仍然是按正常流布局的,这意味着段落的背景色会穿透到 float 下方,而不是被 float 阻隔。
1
2
<div class="float-box">我浮动了</div>
<p>这段文字的盒子依然全宽排列,但行盒会缩短让出空间,所以文字看起来绕过了浮动盒子。背景色会穿透到浮动盒子下面。</p>
想给 float 周围留空隙,必须在 float 元素自身设 margin,跟在后面的 in-flow 元素上设是没用的——因为它们布局时根本不知道 float 存在。
position: absolute 和 position: fixed 是更彻底的脱离。absolute 元素完全从流中消失,后续元素完全当它不存在,连文字环绕都不会有,内容极易重叠。它的偏移量是相对于最近的非 static 祖先来计算的,没有这样的祖先就相对于初始容纳块(通常是 <html>)。fixed 则直接相对于视口定位,滚动也不会影响它。
position: relative 是一个需要特别注意的情况——它不脱离流。relative 元素的偏移只是视觉效果上的移动,它在原位置的空间依然保留着,其他元素仍然按照它的”原位”来布局。可以理解为:坑还留着,只是人往旁边挪了挪。
1
2
3
4
5
6
7
8
9
10
11
12
13
.abspos {
position: absolute;
top: 30px;
right: 30px;
/* 完全脱离流,后面的元素当它不存在,极易造成内容重叠 */
}
.relpos {
position: relative;
bottom: 50px;
left: 50px;
/* 坑还在,只是视觉上位移了。其他元素仍然按原位布局 */
}
Out of flow 与 BFC 的关系
所有脱离流的元素都有一个共同点:它们会自动创建新的 BFC。也就是说,脱离流的那一刻,元素内部就变成了一个独立的小布局,无论外面怎么变化,里面的 block/inline 规则照常运转。事实上根元素 <html> 本身就是 out of flow 的存在——它是整个页面的 BFC 容器,也是这个规则的根源。
Understanding z-index(理解 z-index)
在讨论定位之前我们需要先回答一个问题:当元素重叠时,谁在上面?
最简单的 HTML 页面可以看作二维的:文字、图片、按钮顺着单一渲染流依次排列,没有重叠。但一旦加上定位、transform、opacity 这些属性,重叠就不可避免了。z-index 属性就是用来控制元素在第三条轴,即z 轴,上谁前谁后的。
想象页面是一叠编号的图层:负数层在底部、0 层是默认渲染层、正数层在顶部。数字越大越靠近观察者。
当没有指定任何 z-index 时,所有元素都停在默认的 Layer 0 上。
但事情并没有这么简单。z-index 的”简单”只停留在单个属性上。一旦 HTML 结构变成嵌套的层级,你就会发现 z-index: 9999 也不一定能让元素跑到最上面。原因就是我们接下来要讨论的层叠上下文(stacking context)。
不带 z-index 时的默认层叠
在不使用 z-index 时,元素的堆叠顺序遵循一个固定的规则(从底到顶):
- 根元素的背景和边框(最底层)
- 非定位的后代元素,按 HTML 出现顺序
- 定位的后代元素(position 为 absolute、relative、fixed、sticky),按 HTML 出现顺序
注意这里的”定位元素”指的是 position 不为 static 的元素。这就解释了为什么 position: absolute 的元素总是盖在普通元素上面,因为它们天然在更高的层级上。
1
2
3
4
5
6
7
8
9
10
11
/* 五个 div,DIV #1~#4 有定位,DIV #5 是 static */
/* 虽然 DIV #5 在 HTML 中最后出现,但它仍然在最底层 */
.static {
position: static; /* 非定位,被压在下面 */
}
.absolute {
position: absolute; /* 定位元素,即使出现在前面也盖住 static */
}
.relative {
position: relative; /* 同理 */
}
浮动元素在层叠中的位置
浮动元素的层叠位置介于两者之间。完整的默认堆叠顺序(从底到顶)是:
- 根元素的背景和边框
- 非定位块级元素,按 HTML 出现顺序
- 浮动元素
- 非定位行内元素(inline 后代)
- 定位元素,按 HTML 出现顺序
这里的优先级存在一个看似反直觉的细节:非定位块级元素的内容(文字等 inline 内容)实际上在浮动元素之上。
这就是 float 环绕效果的本质:float 盒子被推到一边,但文字行盒不会跑到底下去,而是缩短后继续显示。换句话说,浮动元素的盒子在非定位元素盒子之上,但文字内容在浮动元素之上。
如果给一个非定位元素加上 opacity < 1,事情会变得古怪:那个元素的背景和边框会”跳”到浮动和定位元素的上方。这是因为 opacity 会创建新的层叠上下文,一旦进入新的上下文,层叠规则就完全重置了。
Stacking context(层叠上下文)
层叠上下文可以把一组元素”打包密封”。里面的元素关起门来自己排谁上谁下,外面只看这个包整体排在哪个位置,不管包里谁最高。就像一个封了口的快递箱:箱子里帽子塞在鞋子上方,但箱子外的书架上,这个箱子整体摆在第几格,跟帽子多高没关系。
这就是理解 z-index “为什么不按数字大小排”的核心:子元素 z-index: 999 冲不出自己的层叠上下文,跨上下文的比较永远以上下文本身为准。
层叠上下文只控制 z 轴(垂直于屏幕的方向),跟 x/y 轴(元素在页面上的平面位置)是两个独立体系。
格式化上下文(BFC、IFC、Flex、Grid)管”元素放在哪里”,层叠上下文管”谁盖在谁上面”。同一个元素可以同时活在一个格式化上下文里(决定平面位置)又是一个层叠上下文(决定 z 轴叠放),两件事各算各的。
什么会创建层叠上下文
除了根元素 <html> 天生就是层叠上下文,以下条件也会触发新建:
position: absolute或relative+z-index不为autoposition: fixed或sticky- flex item 或 grid item +
z-index不为auto opacity小于 1transform、filter、backdrop-filter、perspective、clip-path、mask不为noneisolation: isolatewill-change指定了任何会创建层叠上下文的属性contain: layout或paint
为什么这些属性会创建层叠上下文?答案是它们都需要把元素先画成一整张图,再处理。和格式化上下文的”补丁”逻辑如出一辙,创建层叠上下文也是”顺便”的。
opacity: 1是不透明覆盖,浏览器按像素逐个画、直接盖上去即可,不需要知道底下是什么。opacity: 0.99则需要和底层做颜色混合(”自己的颜色 × 0.99 + 底层颜色 × 0.01”),浏览器只能先把整个元素画到一个离屏缓冲区,再对这个缓冲区整体做透明度混合。transform、filter同理:需要先画成一整张图,再对这张图做旋转、模糊等变换。而”先画成一整张图”这个动作,天然就会形成一个独立渲染单元,子元素的z-index对外部不再有意义——层叠上下文就这样”被顺便创建了”。
opacity: 1不需要离屏缓冲区,直接覆盖即可,所以不创建。isolation: isolate最特别——它什么都不做,只创建层叠上下文,是开发者专用的”打包”开关。
这里有一个面试高频陷阱:opacity: 0.99 和 opacity: 1 虽然视觉上几乎一样,但前者创建了层叠上下文,后者没有。 同样地,transform: scale(1) 看起来什么都没做,但已经悄悄创建了层叠上下文。
嵌套层叠上下文的版本号模型
MDN 给了一个极好的类比:把层叠上下文看作”版本号”。根元素是主版本,嵌套的层叠上下文是次版本。
1
2
3
4
5
6
7
Root
├── ARTICLE #1 (z-index: 5) → 版本号 5.0
├── ARTICLE #2 (z-index: 2) → 版本号 2.0
└── ARTICLE #3 (z-index: 4) → 版本号 4.0
├── SECTION #4 (z-index: 6) → 版本号 4.6
├── SECTION #5 (z-index: 1) → 版本号 4.1
└── SECTION #6 (z-index: 3) → 版本号 4.3
渲染顺序从 2.0 → 4.0 → 4.1 → 4.3 → 4.6 → 5.0,从底到顶。关键洞察:SECTION #4 的 z-index: 6 比 ARTICLE #1 的 z-index: 5 大,但 SECTION #4 仍然在 ARTICLE #1 下面。因为 SECTION #4 的 6 是它父亲 ARTICLE #3 内部的次版本号——对外界来说,整个 ARTICLE #3 就是一个 z-index: 4 的原子单元。ARTICLE #1(5.0)自然在 ARTICLE #3 的整体(相当于 4.x)之上。
一旦理解了”版本号模型”,
z-index的谜团就解开了。子元素z-index: 9999也超不过外面z-index: 1的另一个层叠上下文。因为我们比的不是数字大小,而是谁的”归属上下文”更高。
Using z-index(使用 z-index)
最简单也最容易踩坑的场景:z-index 只对定位元素(position 不为 static)生效。
1
2
3
4
5
6
7
8
/* z-index 只在定位元素上有意义 */
#pos {
position: relative;
z-index: 5; /* 有效 */
}
#static {
z-index: 8; /* 无效!static 元素忽略 z-index */
}
多个元素在同一个层叠上下文内共享相同的 z-index 值时,回退到默认的层叠规则——HTML 源码中后出现的在上。如果它们不在同一个层叠上下文中,z-index 的比较就变成了所属上下文之间的比较,正如上一节讨论的版本号模型。
z-index 可以接受负值,把元素推到 Layer 0 以下。但负值也有上限——它不能低于创建它的层叠上下文的背景和边框层。换句话说,一个层叠上下文的背景和边框,是所有这个上下文内元素的最底层,负 z-index 最多只能叠到背景的上面一层。
From Practice to Spec: BFC、IFC 与层叠上下文的历史
这篇文章里反复出现一个模式:先有经验口诀,后有正式定义。BFC、IFC、层叠上下文这三个概念都是自下而上长出来的,不是 CSS 设计师一开始就想好的。
BFC 最早没有名字。 CSS 1(1996 年)只定义了 block 和 inline,block 竖着排、inline 横着排。后来开发者用 float 做布局,发现给父元素加 overflow: hidden 就能”包住浮动”。
这个行为能生效,说明浏览器引擎在设计时就已经内置了 BFC 的规则:overflow: hidden 能让父容器”看见”内部浮动,不是因为巧合,而是引擎的排版逻辑本来就有一条分支被这个属性值触发了。
概念是隐含的、能用的,只是没有被单独拎出来命名。直到 CSS 2.1(2004 年),规范才正式定义”Block formatting context”,把这个已经存在了 8 年的隐含概念显式化。
层叠上下文也是先有 bug 后有概念。 早期开发者发现 z-index 不按数字大小显示,”明明设了 999 结果还在底下”,踩坑多年才总结出 z-index 是有作用域的。CSS 2.0(1998 年)把这层关系抽象成”stacking context”写入规范,但创建条件到现在还在不断扩充(opacity、transform、filter 等都是后来加的)。
display: flow-root 是这条路上最近的里程碑。 从”发现 BFC 能包含浮动”到”给 BFC 一个专用的属性值”,走了快 20 年。
CSS 的历史不是先设计完再应用的,是开发者在网页越来越复杂的需求中踩坑、总结规律、形成共识,规范委员会再反过来给这些共识一个正式名分。
理解了这一层, flex 和 grid 的设计会更加清晰。它们不是 BFC 的补丁,是吸取了 20 年教训之后重新设计的布局系统;它们创建的是全新的格式化上下文,也意味着在布局场景下 BFC 的退居二线。
但页面结构设计的不断进化不意味着 BFC 在内容流排版的地位不如当年,BFC 的应用场景依然很广泛,能力也依然很强大。
只要开发者写一个 <h1>、或者一个 <div>(没有设 display: flex),它们仍然活在 BFC 或 IFC 里。normal flow 的排版,例如段落堆叠、文字换行、margin 折叠,仍然是 BFC 和 IFC 在调度。Web 上绝大多数的内容流(content flow)还是走的 BFC/IFC 老路。
关于布局树与 DOM 树的节点差异(匿名行盒/块盒、
display:none消失、::before新增),在番外 浏览器渲染原理的布局一节有更完整的讨论。 ↩