网页滚动想整橡皮筋回弹效果,纯CSS咋搞定?

3,237字
14–21 分钟
in

移动端那些丝滑滚动,拉到尽头还能继续拖一截,松手就“嗖”一下弹回去,这玩意儿江湖人称“橡皮筋滚动”或者“回弹滚动”。很多网页默认就有,比如在Safari里上下猛划,内容超出边界几十像素再弹回来,手感倍儿爽。但要是想在一个固定尺寸的盒子里也复刻同款效果,甚至横向整活,纯CSS能不能拿捏?必须能,而且不用JS那些监听滚动位置、计算惯性运动的复杂操作。

目录

啥是橡皮筋回弹

橡皮筋回弹说白了就是滚动容器允许短暂超出边界,然后自动复位。跟现实里拉橡皮筋一个道理——拽过头了,一松手立马弹回原位。浏览器自带的那种叫“滚动边界反弹”,但只对页面根滚动有效。要是自己写一个定高定宽的div,里面内容超出,默认滚动到边界就卡死,没那味。现在用纯CSS两个大招:内外边距制造超出空间,滚动捕捉强制对齐回位。

垂直方向整活

先搭一个会溢出的容器,里头塞几个块级元素。比如做一个纵向轮播卡片组,高度固定,内容撑爆。

<div class="scroll-box">
  <div class="item-stack">
    <div class="card">1号卡片</div>
    <div class="card">2号卡片</div>
    <div class="card">3号卡片</div>
    <div class="card">4号卡片</div>
    <div class="card">5号卡片</div>
  </div>
</div>

基础样式先给上,保证父容器固定尺寸并且能滚动。

.scroll-box {
  width: 240px;
  height: 420px;
  overflow-x: hidden;
  overflow-y: auto;
  border: 2px solid #ccc;
}

.item-stack {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: fit-content;
}

.card {
  width: 100%;
  aspect-ratio: 1;
  background: #f0f0f0;
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
}

现在滚动到最顶上或最底下,边界直接焊死,没有弹力。为了让滚动能超出边界,得在第一个子元素的顶部和最后一个子元素的底部加额外空间。加外边距最稳当,因为外边距算在滚动尺寸里。

.scroll-box > .item-stack > .card:first-child {
  margin-top: 120px;
}

.scroll-box > .item-stack > .card:last-child {
  margin-bottom: 120px;
}

这么一搞,往下划的时候最底下的卡片底下多了120像素空白,继续划就能越过最后一个卡片进入空白区。往上划同理,第一个卡片顶上也有120像素空白。但光有超出不行,松手后得让它弹回去。这时候请出滚动捕捉全家桶。

.scroll-box {
  scroll-snap-type: y mandatory;
}

.scroll-box > .item-stack > .card {
  scroll-snap-align: start;
}

.scroll-box > .item-stack > .card:first-child {
  margin-top: 120px;
  scroll-snap-align: start;
}

.scroll-box > .item-stack > .card:last-child {
  margin-bottom: 120px;
  scroll-snap-align: end;
}

注意最后一张卡片用scroll-snap-align: end,这样滚动到最底下空白区松手时,最后一张卡片的底部会贴合容器底部,而不是用顶部对齐。第一个卡片用start保证顶部贴合。中间卡片统一用start,上下划动时每个卡片顶部对齐,手感就很规整。如果容器里只有一个长内容块,那就在那个块上同时加margin-top和margin-bottom,并且把scroll-snap-align分别设为start和end。

属性作用
scroll-snap-typey mandatory纵向强制捕捉
scroll-snap-alignstart元素顶部对齐
scroll-snap-alignend元素底部对齐

水平方向操作

横向橡皮筋回弹同样套路,只不过把上下换成左右。比如做一个横向滑动的图片轮播,容器宽度固定,内容水平排列超出。

<div class="h-scroll">
  <div class="h-track">
    <div class="h-item">A</div>
    <div class="h-item">B</div>
    <div class="h-item">C</div>
    <div class="h-item">D</div>
    <div class="h-item">E</div>
  </div>
</div>

基础样式改成横向flex,父容器横向滚动。

.h-scroll {
  width: 360px;
  height: 200px;
  overflow-x: auto;
  overflow-y: hidden;
  border: 2px solid #999;
}

.h-track {
  display: flex;
  flex-direction: row;
  width: fit-content;
  height: 100%;
}

.h-item {
  height: 100%;
  aspect-ratio: 1;
  background: #e0e0e0;
  margin-right: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
}

然后给第一个和最后一个子元素加左右外边距。注意横向滚动超出边界需要左边距和右边距。

.h-scroll > .h-track > .h-item:first-child {
  margin-left: 100px;
}

.h-scroll > .h-track > .h-item:last-child {
  margin-right: 100px;
}

加上滚动捕捉,这次把scroll-snap-type改成x mandatory,对齐方向用start表示左边缘,end表示右边缘。

.h-scroll {
  scroll-snap-type: x mandatory;
}

.h-scroll > .h-track > .h-item {
  scroll-snap-align: start;
}

.h-scroll > .h-track > .h-item:first-child {
  margin-left: 100px;
  scroll-snap-align: start;
}

.h-scroll > .h-track > .h-item:last-child {
  margin-right: 100px;
  scroll-snap-align: end;
}

现在左右滑动超出边界时会多出一段空白,松手后自动弹回,第一个元素左边缘对齐容器左边缘,最后一个元素右边缘对齐容器右边缘。中间每个元素滑动时左边缘自动对齐,横向浏览体验直接拉满。如果容器里只有一个超宽的内容块,同样在左右两边加margin,然后分别设置scroll-snap-align: startend

搞这套方案的时候有个坑:外边距数值得根据实际效果微调。给120px还是80px取决于想让超出多少像素,一般80到150之间手感比较接近原生。另外如果内容是通过JS动态增删的,记得保证:first-child:last-child仍然准确指向真正的首尾元素。还有滚动捕捉跟overflow结合时,父容器不能有transform或者will-change图层属性,否则捕捉可能失灵。移动端测试时,某些浏览器对scroll-snap-typemandatory强制捕捉会稍微掉帧,改成proximity也能凑合用,但回弹的确定感会弱一些。