transform的副作用——从失效的fixed说起

2020-08-28

失效的 position: fixed

现在有很多设计都是做相对视口固定的效果,例如:阅读文章时有一个固定的返回顶部按钮、电商页面有一个获取帮助的固定按钮。

image-20200828183627972
知乎上的固定悬浮按钮

人尽皆知的是 position:fixed 可以用来做这种相对视口固定的效果。但笔者在使用 position:fixed 时曾经遇到过问题:设置fixed的元素嵌套在一个使用了transform的祖先元素中,导致了fixed失效。简易的还原例子可以看这个codepen

我们可以在MDN上看到关于这个问题的描述:

当元素祖先的 transform, perspectivefilter 属性非 none 时,容器由视口改为该祖先。

那解决方案也十分明显了,有以下可以待选的方案:

  • 将元素放置在transform的父元素之外。比如react就提供了Portals API,提供了将子节点渲染到父组件之外的DOM节点之外的方法。不借助任何框架的情况下,也可以用DOM API去解决这个问题。
  • 在运算topleft等定位属性时加上父元素造成的偏移。但是这就失去了fixed的特性,变得和absolute一样了。

深入一点

MDN 这里的描述显然是说的一个结果。那么是什么机制造成了这个结果呢?这时候就要翻出 w3c 的标准来看了。在CSS Transforms Module 标准的 The Transform Rendering Model章节详细介绍了这个现象的来源。标准中明确写出:

For elements whose layout is governed by the CSS box model, the transform property does not affect the flow of the content surrounding the transformed element. However, the extent of the overflow area takes into account transformed elements. This behavior is similar to what happens when elements are offset via relative positioning. Therefore, if the value of the overflow property is scroll or auto, scrollbars will appear as needed to see content that is transformed outside the visible area. Specifically, transforms can extend (but do not shrink) the size of the overflow area, which is computed as the union of the bounds of the elements before and after the application of transforms.

For elements whose layout is governed by the CSS box model, any value other than none for the transform property results in the creation of a stacking context. Implementations must paint the layer it creates, within its parent stacking context, at the same stacking order that would be used if it were a positioned element with z-index: 0. If an element with a transform is positioned, the z-index property applies as described in [CSS2], except that auto is treated as 0 since a new stacking context is always created.

For elements whose layout is governed by the CSS box model, any value other than none for the transform property also causes the element to establish a containing block for all descendants. Its padding box will be used to layout for all of its absolute-position descendants, fixed-position descendants, and descendant fixed background attachments.

翻译和总结一下,transform 对其他元素渲染的副作用有:

  • transform 的元素会影响overflow area (溢出区域)。也就是说,使用transform使得元素移出了父元素之外的话,在父元素上使用overflow: scrolloverflow:auto的情况下,父元素将会展示出滚动条。
  • transform 的元素会创造一个stack context (层叠上下文),造成内部和外部的z-index相互独立。
  • transform 的元素将会创建一个 containing block (包含块),所有的positionabsolutefixed的子元素、以及设置了background-attachment的背景将会相对于该元素的 padding box 布局。

这篇文章如果要找出一个最重要的地方,那就是这三个副作用了。

image-20200828185802850

当前的 CSS Transforms 标准

第三个规则则是造成了fixed失效的直接原因。除了造成笔者遇到的问题的第三条之外,我们可以用例子来详细看看这其余的两个副作用:

🌰 overflow area

image-20200828190610701
需要拖动滑动条才能看见的子元素
<div class="container">
  <div class="transformed-item"></div>
</div>
.container{
  height: 300px;
  background: #ffb6b9;
  overflow: scroll;
}

.transformed-item{
  height: 100px;
  width: 100px;
  background-color: rgba(250,227,217,1);
  transform: translateY(400px);
}

你可以点击这个 codepen 看看实际效果

🌰 stack context

stack context(层叠上下文)内部元素的z-index才能有互相作用。对于外部则不起作用,而是以层叠上下文整体的z-index(也就是根元素z-index)去相互比较。

image-20200828191604439
z-index为10的元素叠在了z-index100 的元素之上
<div class="normal_container">
  <div class="normal">z-index:100</div>
</div>
<div class="transform_container">
  <div class="transformed">z-index:10</div>
</div>
div{
  height: 200px;
  width: 200px;
  padding: 16px;
  color: white;
}

.normal_container{
  z-index:0;
}

.normal{
  background: #ffb6b9;
  z-index:100;
}

.transform_container{
  z-index: 1;
  transform: translate(100px,-100px);
}

.transformed{
  background: #fae3d9;
  z-index:10;
}

你可以点击这个 codepen 看看实际效果

containing block 包含块

为了更加完整地理解我们遇到的这个问题,需要解释另一个概念containing block(包含块)。

包含块可以做什么

这个概念在 CSS2 被提出,是作为元素渲染的非常基本的概念。一些属性如width, height, padding,margin, top, left, right, bottom等等与布局相关的属性被设置为百分比时,其实就是相比于包含块的相对单位。除此之外,包含块还决定了overflow (溢出)行为。当内部的元素超出了其包含块的范围,就会造成溢出。

包含块如何确定

大多数情况下,包含块就是这个元素最近的祖先块元素内容区。但是对于position 不为static的元素来说,规则有所变化:

  • 如果 position 属性为 absolute,包含块就是由它的最近的 position 的值不是 static 的祖先元素的内边距区的边缘组成。
  • 如果 position 属性是 fixed,在连续媒体的情况下(continuous media)包含块是 viewport ,在分页媒体(paged media)下的情况下包含块是分页区域(page area)。
  • 如果 position 属性是 absolutefixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:
    • transformperspective 不为none
    • filter不为none
    • will-changetransformperspective
    • containpaint (这个属性虽然冷门很有意思,希望大家都看看)

也就是说,不光光是position:fixed,连position:absolute也会被transform生成的包含块所影响。

一点思考

为什么w3c会为了transform单独这样设计呢?我觉得最大的考虑点还是w3c委员会认为transform的元素及其子元素应该是一个整体。这个设计的好处其一是更符合逻辑,可以类比一下ppt、figma、sketch中已组合的元素,他们的缩放、位移、动画等等变换都是一起的;其二是这样设计可以尽量减少对外部布局的影响,也减少外部对transform内部布局的影响,这样的话,浏览器引擎可以减少很多计算量。

当然我也没有看 w3c 会议的相关邮件,这仅仅是我的猜测~