先记住这个答案
包含块就是定位元素的参照框,top、left 和百分比宽度都相对它计算。通常 absolute 找最近的非 static 祖先,fixed 直接相对视口。但规范额外规定:只要祖先的 transform 不是 none,它就会给所有后代建立包含块。于是 absolute 子元素会相对这个祖先定位,fixed 子元素也不再贴着视口,而是跟着这个祖先走。这样设计是因为 transform 会把元素连同后代当成一个整体做变换,后代如果还相对视口定位,就没法和祖先一起被旋转、缩放。
- 变换祖先可成为定位包含块
fixed弹窗被困住,是最常见的线上表现- 滤镜和透视等也可能建立包含块
- 修法:弹窗用 Portal 挂到
body下
最常见的线上事故:弹窗被困在卡片里
一个卡片做了 hover 上浮动画,写了 transform。卡片里有个按钮,点了弹出 fixed 全屏遮罩。结果遮罩只盖住了卡片那一块。
fixed 遮罩相对了卡片HTML<style>
.card { transform: translateY(0); } /* 为了过渡动画预先写的 */
.card:hover { transform: translateY(-4px); }
.mask { position: fixed; inset: 0; background: rgb(0 0 0 / 0.5); }
</style>
<div class="card">
<button>删除</button>
<!-- 期望盖满整个视口,实际只盖住 .card 这个矩形 -->
<div class="mask"></div>
</div>.card 的 transform 是 translateY(0),看起来没有移动,但它不是 none,已经成了包含块。.mask 的 inset: 0 于是相对 .card 计算,只铺满卡片。页面滚动时它还会跟着卡片一起滚走,如果卡片有 overflow: hidden,遮罩还会被裁掉。
absolute 的情况类似。原本子元素会越过卡片,去找外面某个写了 position: relative 的容器,现在卡片自己就成了参照物,位置会整体偏掉。
为什么规范要这么设计
transform 改变的是元素的整个坐标系。把一个元素旋转 30 度,它的后代也要一起转,否则看起来就是散架的。如果 fixed 后代还按视口定位,浏览器就得把它从这个整体里拆出来单独绘制,旋转、缩放对它是否生效就说不清了。所以规范干脆规定:变换过的元素给所有后代建立包含块,后代的定位都在它的坐标系里算。
- 同样会抢包含块的属性
- 不止
transform。perspective不为none、filter不为none、backdrop-filter不为none、will-change里写了transform或filter、contain取paint或layout,都会让元素成为absolute和fixed后代的包含块。单独的translate、rotate、scale属性不为none时也一样。 排查「fixed元素不贴视口」时,就沿着祖先链往上找这几个属性。在开发者工具里选中元素,一路看父级的 Computed 面板,比猜快得多。
修法:让弹窗离开这棵子树
最可靠的办法是不让弹窗待在变换过的祖先里面。React 用 createPortal 把遮罩渲染到 document.body 下,Vue 用 <Teleport to="body">,组件逻辑还在原来的位置,DOM 却挂到了最外层,fixed 就重新相对视口了。主流组件库的 Modal、Popover 默认就是这么做的。
如果改不了结构,就去掉祖先上的 transform。只是为了动画预先写的 translateY(0),可以删掉静态值,只在 hover 时加;为了 GPU 加速随手写的 translateZ(0),确认没有必要后也可以去掉。
容易答错的地方
- 以为只有写了
position的祖先才算 absolute找包含块并不只看position。祖先有transform、filter这些属性时,即使position是static,也会成为包含块。- 以为
translate(0)不算变换 - 判断标准是值是不是
none,不看有没有实际位移。translate(0)、scale(1)、translateZ(0)都会建立包含块。 - 以为只影响
fixed absolute也受影响,只是它本来就相对某个祖先定位,偏移不如fixed明显,更容易被忽略。
面试官还会怎么问?
transform 还会带来哪些副作用?
它会创建新的层叠上下文,所以子元素的 z-index 再大也只在这个元素内部比较,出不去。弹窗被别的模块盖住,经常也是这个原因。
position: sticky 会受 transform 影响吗?
sticky 的参照物是最近的滚动容器,不是包含块,所以祖先加 transform 一般不会让它失效。真正让 sticky 失效的,多半是中间某层祖先设了 overflow: hidden 或 auto,把滚动容器换掉了。
既然有这么多副作用,动画为什么还推荐用 transform?
改 top、left 做位移,每一帧都要重新布局;transform 不影响布局,浏览器可以只在合成阶段移动图层,动画更流畅。副作用是可控的,只要弹窗这类需要相对视口的元素不放在它里面就行。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。