CSS position:sticky 完全解读
Sticky positioning is a hybrid of relative and fixed positioning. The element is treated as relative positioned until it crosses a specified threshold, at which point it is treated as fixed positioned.
_
以上是 MDN 对 position: sticky 的解释。这个属性在很长的一段时间内被我遗忘了,大部分业务不会用到花里胡哨的粘性布局。直到最近深入研究注重内容展示和动画效果的产品站,才重新拾起来。
粘性可以概括为:某些元素在一定滚动范围内固定在屏幕上的布局。粘在屏幕上的元素可以配合滚动改变样式,形成滚动动画,苹果官网大量使用了这种布局技巧。
如上面的例子中,手机在滚动至屏幕中央时被粘住并放大,消失,最终成为黑色的背景,苹果使用 ScrollMagic 实现这种效果。这个库在监听滚动的同时,用比较 hack 的方式实现了元素粘在屏幕上的效果,效果也不错,我们的产品站 miui.com 也使用了这个库。不过在现代浏览器下,我们可以使用性能更高的,更加优雅的方式来实现粘性布局。
正确理解 Sticky
Sticky 使用起来非常简单,但有的地方又很迷,可能许多人都遇到过 Sticky 元素失效的问题,看完下面的解释,你就可以弄明白 Sticky 为什么经常失效了。
Sticky 容器
我们在为某个元素设置 Sticky 属性的时候,相当于变相将它的父元素设置为了 Sticky 容器。Sticky 元素一定是固定在 Sticky 容器中的,当 Sticky 元素将要超出容器时,粘性消失,Sticky 元素会停在粘性消失的位置同 Sticky 容器一起滚动。
<h1 class="sticky-item">Sitcky Content</h1>
<div class="placeholder"></div>
.placeholder {
height: 150vh;
}
.sticky-item {
position: sticky;
top: 0;
}
尝试编写上面的这段代码,滚动页面,可以发现目标元素 sticky-item
被“视口”触发了粘性效果。此时目标元素的父元素也就是 Sticky 容器是 body
,此时 Sticky 容器的高度为 placeholder
的高度加上目标元素的高度,所以目标元素可以在容器中滑动。
滑动空间
<div class="sticky-wrapper">
<h1 class="sticky-item">Sitcky Content</h1>
</div>
<div class="placeholder"></div>
如果我们为目标元素包裹一个元素,那么现在的 Sticky 容器变成了 sticky-wrapper
这个元素,此时 Sticky 容器的高度与目标元素相等,再滚动页面发现视口无法触发粘性效果。
.sticky-wrapper {
height: 300px;
}
如果为 Sticky 容器设置一个大于目标元素的高度,会发现目标元素再次具有了粘性效果,只不过这次粘性效果只持续了很短的距离。所以,我们可以得出结论,Sticky 是否生效取决于 Sticky 容器是否为 Sticky 元素预留了滑动空间。
滚动祖先
根据 MDN 的介绍,一个 Sticky 元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上。这句话是有歧义的,更恰当的解释应该是:一个 Sticky 元素一定固定在父元素(滚动容器)中,只有离它最近的一个拥有“滚动机制”的祖先可以触发 Sticky 效果。如果一个元素的 overflow
为 是 hidden
, scroll
, auto
, 或 overlay
时,则认为这个元素拥有”滚动机制“。
Sticky 元素可以设置 top
bottom
left
right
属性, top
与 bottom
不能同时设置,优先识别 top
,同理优先识别 left
。top
表示 Sticky 元素上边与滚动祖先上边的距离, bottom
表示 Sticky 元素下边与滚动祖先下边的距离, left
right
同理。
<div class="sticky-wrapper">
<h1 class="sticky-item">Sitcky Content</h1>
<div class="placeholder"></div>
</div>
.placeholder {
height: 150vh;
}
.sticky-wrapper {
margin-top: 50vh;
height: 300px;
overflow: scroll;
}
.sticky-item {
position: sticky;
top: 0;
}
现在,目标元素的滚动祖先就是 Sticky 容器,所以它粘在了 Sticky 容器的上边。
确定滑动空间
如何确定滑动空间是使用 Sticky 最迷的地方,所幸,引入了 Sticky 容器这个概念可以帮助我们快速记忆(没有必要深入研究)。首先,当 Sticky 元素的高度大于或等于 Sticky 容器时,是一定没有滑动空间的。
我们将 Sticky 元素居中定位至 Sticky 容器中来观察,当指定 top 时:
当指定 bottom 时:
我们发现,在这两种情况下,Sticky 元素的滑动空间是完全相反的,由此我们可以总结出一个规律:
指定了 top
则滑动空间就是 Sticky 元素底部与 Sticky 容器底部的距离,指定了 bottom
则滑动空间就是 Sticky 元素顶部与 Sticky 顶部的距离, left
right
同理。
实际项目中,为了减少心智负担,建议只使用 top
或 left
,在少数特殊的情况下再考虑使用 bottom
和 right
。
Sticky & 盒模型
由 CSS 原生支持的 sticky 无疑是好用的,但想要做到指哪打哪,就必须得了解盒模型下 Sticky 元素的表现。我们为外层元素添加了 padding
,为 Sticky 元素添加了 padding
border
margin
。
可以发现:
- Sticky 元素的
border-edge
粘在了 Trigger 上; - Sticky 元素的
margin-edge
将要超出父元素的content-edge
时,粘性消失。
现在,我们可以完善一下滑动空间的规律:
指定了 top
则滑动空间就是 Sticky 元素下 margin-edge
与 Sticky 容器下 content-edge
的距离,指定了 bottom
则滑动空间就是 Sticky 元素上 margin-edge
与 Sticky 容器上 content-edge
的距离, left
right
同理。
巧用 Sticky
- Sticky 元素与 Sticky 容器是强绑定的,Sticky 容器可以使用任意定位方式布局,调整 Sticky 容器的尺寸来影响 Sticky 元素的滑动距离;
- 使用 Intersection Observer API (推荐)或监听滚动来为 Sticky 元素或容器添加动画效果。调整 Sticky 元素的定位值可以创建视差效果,这是实现视差最优雅的方式;
- 尽量让视口最为 Sticky 元素的滚动祖先,调试起来很方便。可以使用下面的脚本来检查 Sticky 元素的滚动祖先:
function checkParents(el) {
if(!el.parentNode) {
console.log(el)
return;
} else {
var p = el.parentNode;
var style = p instanceof Element && getComputedStyle(p);
if(style && style.overflow !== 'visible') {
console.log(p)
} else {
checkParents(p);
}
}
}