先记住这个答案
基本是对的,但要补两点。第一,包含块是最近一个 position 为 relative、absolute、fixed 或 sticky 的祖先,定位参照的是它的 padding box,也就是 top: 0 贴在祖先 border 的内侧,不是内容区的内侧。一个都找不到时用初始包含块,也就是和视口一样大、放在文档起点的矩形。第二,祖先有 transform、filter、perspective 等属性时,即使没设定位也会成为包含块。中间那些没定位的祖先不参与定位计算,但会影响元素没写偏移量时的默认位置,而且它们的 overflow: hidden 裁不掉这个元素。
- 包含块是最近的非
static定位祖先 - 边界是祖先的 padding box,不是内容区
- 部分变换与滤镜祖先也可建包含块
- 没写偏移量时,元素停在原本在流中的位置
用一个三层结构看清谁是包含块
外层 .card 设了定位,中间 .body 没设,最里面是绝对定位的角标。
<style>
.card { position: relative; padding: 20px; border: 4px solid #333; }
.body { padding: 30px; overflow: hidden; } /* 没有定位 */
.badge { position: absolute; top: 0; right: 0; }
</style>
<div class="card">
<div class="body">
<span class="badge">NEW</span>
<p>正文内容</p>
</div>
</div>
<!-- .badge 贴在 .card 的边框内侧右上角,不在 .body 的角上 -->
<!-- .body 的 padding 和 overflow: hidden 对它都不起作用 -->.badge 往上找,.body 是 static,跳过;.card 是 relative,就是它了。所以 top: 0; right: 0 相对 .card 的 padding box 算,角标贴在 .card 的 border 内侧,.card 的 padding: 20px 不会把它往里推。.body 的 padding: 30px 更是完全不相关。
这也说明了包含块边界为什么是 padding box:border 在外面,padding 在里面。给 .badge 设 width: 100% 时,百分比也是按 .card 的 padding box 宽度算的,会包括左右 padding。
非定位祖先在哪些地方有影响
中间层不是包含块,并不代表它完全没影响。有两个地方它还在起作用,面试时说出来能加分。
- 没写偏移量时的静态位置
- 如果
.badge只写了position: absolute,top、left都是auto,它不会跑到.card的左上角,而是停在「假如它还在普通流里」会出现的位置,也就是.body内容区的开头。这叫静态位置。所以非定位祖先的padding、前面兄弟元素的排布,都会影响这个位置。只要写了偏移量,比如top: 0,垂直方向就改为相对包含块计算。 overflow: hidden裁不到它overflow的裁剪只作用于「以它为包含块,或者包含块在它里面」的后代。上面的例子里,.badge的包含块.card在.body外面,所以.body的overflow: hidden裁不掉角标,角标可以超出.body显示。很多人做下拉菜单时,发现外层overflow: hidden没裁住菜单,或者反过来想让它不被裁,原因都在这里。- 滚动容器里的定位
- 同理,非定位的滚动容器滚动时,绝对定位的后代不会跟着滚,因为它是相对外面的包含块定位的。想让它跟着内容滚动,要给滚动容器加
position: relative。
不设定位也会变成包含块的属性
除了 position,下面这些属性值不为 none 的祖先也会成为绝对定位元素的包含块:transform、perspective、filter、backdrop-filter,will-change 的值里包含 transform 等属性,以及 contain: layout 或 contain: paint。它们对 position: fixed 的影响更常被提起:弹窗本该相对视口固定,外层有个 transform: translateZ(0) 做硬件加速,弹窗就改成相对这个祖先定位,跟着滚走了。排查时在开发者工具里一路往上看祖先的这些属性。
容易答错的地方
- 以为参照的是父元素
- 绝对定位参照的是最近的定位祖先,不是父元素。父元素没设定位时会被跳过,一直找到
html之上的初始包含块。有时候元素看起来位置对,只是碰巧页面没滚动、父元素也在左上角。 - 以为边界是内容区
- 包含块用的是 padding box。给容器加了
padding: 20px,里面top: 0; left: 0的元素还是贴在border内侧,不会自动往里缩20px。想要内缩就自己写top: 20px或者inset: 20px。 - 以为
overflow: hidden一定能裁住子元素 - 只有当
overflow所在元素是包含块或者包含块在它里面时才能裁住。中间隔着一个定位祖先的情况,裁剪就失效了。
面试官还会怎么问?
初始包含块和视口有什么区别?
初始包含块尺寸和视口一样,但位置固定在文档开头。页面滚动后,相对初始包含块定位的绝对定位元素会跟着文档一起滚走。position: fixed 默认才是相对视口,滚动时不动。
如果定位祖先是一个行内元素呢?
包含块是这个行内元素第一个和最后一个行内盒子的 padding box 围成的矩形。行内元素跨行时,这个矩形的形状会比较反直觉,实际项目里一般会把它改成 inline-block 避免歧义。
position: fixed 的包含块怎么确定?
默认是视口。但只要某个祖先有 transform、filter、perspective 等属性,它就会变成 fixed 元素的包含块,fixed 退化成类似 absolute 的效果。所以全局弹窗通常用 Portal 挂到 body 下,避开这些祖先。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。