移动端那些丝滑滚动,拉到尽头还能继续拖一截,松手就“嗖”一下弹回去,这玩意儿江湖人称“橡皮筋滚动”或者“回弹滚动”。很多网页默认就有,比如在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-type | y mandatory | 纵向强制捕捉 |
| scroll-snap-align | start | 元素顶部对齐 |
| scroll-snap-align | end | 元素底部对齐 |
水平方向操作
横向橡皮筋回弹同样套路,只不过把上下换成左右。比如做一个横向滑动的图片轮播,容器宽度固定,内容水平排列超出。
<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: start和end。
搞这套方案的时候有个坑:外边距数值得根据实际效果微调。给120px还是80px取决于想让超出多少像素,一般80到150之间手感比较接近原生。另外如果内容是通过JS动态增删的,记得保证:first-child和:last-child仍然准确指向真正的首尾元素。还有滚动捕捉跟overflow结合时,父容器不能有transform或者will-change图层属性,否则捕捉可能失灵。移动端测试时,某些浏览器对scroll-snap-type的mandatory强制捕捉会稍微掉帧,改成proximity也能凑合用,但回弹的确定感会弱一些。
