HomeAuthorContactSearch

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 的解释。这个属性在很长的一段时间内被我遗忘了,大部分业务不会用到花里胡哨的粘性布局。直到最近深入研究注重内容展示和动画效果的产品站,才重新拾起来。

粘性可以概括为:某些元素在一定滚动范围内固定在屏幕上的布局。粘在屏幕上的元素可以配合滚动改变样式,形成滚动动画,苹果官网大量使用了这种布局技巧。

Kapture 2020-05-19 at 17.10.03.gif

如上面的例子中,手机在滚动至屏幕中央时被粘住并放大,消失,最终成为黑色的背景,苹果使用 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 元素预留了滑动空间。Kapture 2020-05-19 at 21.01.03.gif

滚动祖先

根据 MDN 的介绍,一个 Sticky 元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上。这句话是有歧义的,更恰当的解释应该是:一个 Sticky 元素一定固定在父元素(滚动容器)中,只有离它最近的一个拥有“滚动机制”的祖先可以触发 Sticky 效果。如果一个元素的 overflow 为 是 hiddenscrollauto, 或 overlay 时,则认为这个元素拥有”滚动机制“。

Sticky 元素可以设置 top bottom left right 属性, topbottom 不能同时设置,优先识别 top ,同理优先识别 lefttop 表示 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 时:

Kapture 2020-05-20 at 13.13.08.gif

当指定 bottom 时:

Kapture 2020-05-20 at 13.15.12.gif

我们发现,在这两种情况下,Sticky 元素的滑动空间是完全相反的,由此我们可以总结出一个规律:

未命名 6.svg

指定了 top 则滑动空间就是 Sticky 元素底部与 Sticky 容器底部的距离,指定了 bottom 则滑动空间就是 Sticky 元素顶部与 Sticky 顶部的距离, left right 同理。

实际项目中,为了减少心智负担,建议只使用 topleft,在少数特殊的情况下再考虑使用 bottom 和 right 。

Sticky & 盒模型

由 CSS 原生支持的 sticky 无疑是好用的,但想要做到指哪打哪,就必须得了解盒模型下 Sticky 元素的表现。我们为外层元素添加了 padding ,为 Sticky 元素添加了 padding border margin 。

Kapture 2020-05-20 at 10.59.27.gif


可以发现:

  • 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);
        }
    }
}