CSS scroll-snap如何实现每次滑动仅切换一个区块 不受滑动速度影响
问题说明
水平滚动容器应用CSS scroll-snap 属性后,快速向右滑动时会直接从第一个区块跳过第二个区块,停在第三个区块,需要实现无论滑动速度快慢,每次滑动仅切换一个区块的效果。
原始复现代码如下:
.slider { border: 4px solid black; width: 300px; height: 200px; overflow: auto; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: grid; grid-template-columns: repeat(10, 100%); scroll-snap-type: both mandatory; } .slider::-webkit-scrollbar { height: 0; } .slds { color: black; width: 100%; height: 100%; list-style: none; font-size: 30px; scroll-snap-align: center; } .slds:nth-child(1){background: red;} .slds:nth-child(2){background: orange;} .slds:nth-child(3){background: green;}
<div class="slider"> <div class="slds">1</div> <div class="slds">2</div> <div class="slds">3</div> </div>
原因分析
原生scroll-snap默认逻辑是根据滑动结束时的动量、最终停留位置计算距离最近的吸附点,没有默认限制单次滑动的跨越步长,因此大力度快速滑动时会直接跨过中间区块。
解决方案
方案1:纯CSS实现(适配现代浏览器)
如果不需要兼容Safari 14及以下版本,直接给吸附子项添加scroll-snap-stop: always属性即可,该属性会强制滚动过程中不能跳过任何吸附点,滑动时无论速度多快,都会停在经过的第一个吸附位置。
修改后的.slds样式如下:
.slds { color: black; width: 100%; height: 100%; list-style: none; font-size: 30px; scroll-snap-align: center; scroll-snap-stop: always; /* 新增该行 */ }
同时建议将.slider的scroll-snap-type从both mandatory改为x mandatory,因为当前容器是水平滚动,不需要纵向吸附逻辑,避免不必要的计算。
方案2:JS辅助实现(全版本兼容)
如果需要兼容更低版本浏览器,可以通过JS拦截滚动逻辑,强制每次滚动仅移动一个区块宽度:
- 记录当前激活的区块索引,监听容器的滚动事件
- 通过滚动偏移量判断滑动方向
- 手动计算相邻区块的滚动位置,调用平滑滚动API移动到目标位置,滚动过程中临时锁定事件避免重复触发
完整实现代码如下:
/* CSS部分保留scroll-snap相关属性提升体验 */ .slider { border: 4px solid black; width: 300px; height: 200px; overflow: auto; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); display: grid; grid-template-columns: repeat(3, 100%); scroll-snap-type: x mandatory; overscroll-behavior: contain; } .slider::-webkit-scrollbar { height: 0; } .slds { color: black; width: 100%; height: 100%; list-style: none; font-size: 30px; scroll-snap-align: center; } .slds:nth-child(1){background: red;} .slds:nth-child(2){background: orange;} .slds:nth-child(3){background: green;}
<div class="slider"> <div class="slds">1</div> <div class="slds">2</div> <div class="slds">3</div> </div> <script> const slider = document.querySelector('.slider') const slideWidth = slider.clientWidth let currentIndex = 0 let isScrolling = false slider.addEventListener('scroll', () => { if (isScrolling) return isScrolling = true // 判断滚动方向计算目标索引 const newIndex = Math.round(slider.scrollLeft / slideWidth) const targetIndex = newIndex > currentIndex ? currentIndex + 1 : currentIndex - 1 // 边界判断避免越界 const safeTargetIndex = Math.max(0, Math.min(targetIndex, slider.children.length - 1)) // 平滑滚动到目标区块 slider.scrollTo({ left: safeTargetIndex * slideWidth, behavior: 'smooth' }) currentIndex = safeTargetIndex // 滚动动画结束后解锁 setTimeout(() => { isScrolling = false }, 300) }) </script>
注意:JS实现中需要根据实际的滚动动画时长调整
setTimeout的延迟时间,避免动画未结束就解锁导致滚动跳变。
内容的提问来源于stack exchange,提问作者Kamal Kimo
相关产品推荐
相关产品推荐

