实现TikTok风格的垂直视频分屏滚动效果,核心是保证用户每次滑动后屏幕恰好完整展示一个视频,并且滑动结束后自动吸附到对应视频的位置,同时支持触摸滑动和鼠标滚轮两种操作方式。这种效果可以提升用户的观看体验,避免视频被截断或者滑动位置不准确的问题。

核心实现原理
整个效果的实现可以分为三个核心部分:首先是布局层面,每个视频容器占满整个可视区域,多个视频容器垂直排列;其次是交互层面,监听用户的触摸滑动和鼠标滚轮事件,计算滑动的距离和方向;最后是吸附逻辑,当滑动结束后,根据当前滑动的位置判断应该展示哪个视频,然后通过动画将对应视频滚动到可视区域顶部。
布局结构设计
首先我们需要构建基础的HTML结构,每个视频项占满整个屏幕,容器设置固定高度,内部视频元素自适应容器大小:
<div class="video-scroll-container">
<div class="video-item">
<video src="video1.mp4" loop muted autoplay></video>
</div>
<div class="video-item">
<video src="video2.mp4" loop muted autoplay></video>
</div>
<div class="video-item">
<video src="video3.mp4" loop muted autoplay></video>
</div>
</div>
对应的CSS样式需要保证每个视频项占满屏幕,容器支持垂直滚动并隐藏滚动条:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
overflow: hidden;
}
.video-scroll-container {
height: 100vh;
overflow-y: scroll;
scroll-snap-type: y mandatory;
/* 隐藏滚动条 */
scrollbar-width: none;
}
.video-scroll-container::-webkit-scrollbar {
display: none;
}
.video-item {
height: 100vh;
scroll-snap-align: start;
position: relative;
}
.video-item video {
width: 100%;
height: 100%;
object-fit: cover;
}
交互逻辑实现
虽然CSS的scroll-snap-type属性已经可以实现基础的吸附效果,但是为了兼容更多场景以及自定义滑动逻辑,我们可以通过JavaScript监听触摸和滚轮事件来完善交互:
// 获取滚动容器
const scrollContainer = document.querySelector('.video-scroll-container');
// 记录触摸起始位置
let touchStartY = 0;
// 记录是否正在执行吸附动画,避免重复触发
let isAnimating = false;
// 单个视频项的高度,即屏幕高度
const itemHeight = window.innerHeight;
// 监听触摸开始事件
scrollContainer.addEventListener('touchstart', (e) => {
if (isAnimating) return;
touchStartY = e.touches[0].clientY;
});
// 监听触摸结束事件
scrollContainer.addEventListener('touchend', (e) => {
if (isAnimating) return;
const touchEndY = e.changedTouches[0].clientY;
// 计算滑动距离,正数表示向下滑,负数表示向上滑
const deltaY = touchEndY - touchStartY;
// 滑动阈值,超过50px才触发切换
const threshold = 50;
// 当前已经滚动的距离
const currentScrollTop = scrollContainer.scrollTop;
// 计算当前所在的视频索引
let currentIndex = Math.round(currentScrollTop / itemHeight);
// 根据滑动距离调整目标索引
if (deltaY < -threshold) {
// 向上滑,切换到下一个视频
currentIndex = Math.min(currentIndex + 1, scrollContainer.children.length - 1);
} else if (deltaY > threshold) {
// 向下滑,切换到上一个视频
currentIndex = Math.max(currentIndex - 1, 0);
}
// 执行吸附动画
scrollToIndex(currentIndex);
});
// 监听鼠标滚轮事件
scrollContainer.addEventListener('wheel', (e) => {
if (isAnimating) return;
e.preventDefault();
// 滚轮向下滚,切换到下一个视频
let currentIndex = Math.round(scrollContainer.scrollTop / itemHeight);
if (e.deltaY > 0) {
currentIndex = Math.min(currentIndex + 1, scrollContainer.children.length - 1);
} else {
// 滚轮向上滚,切换到上一个视频
currentIndex = Math.max(currentIndex - 1, 0);
}
scrollToIndex(currentIndex);
}, { passive: false });
// 滚动到指定索引的视频项
function scrollToIndex(index) {
isAnimating = true;
const targetScrollTop = index * itemHeight;
// 使用requestAnimationFrame实现平滑动画
const startScrollTop = scrollContainer.scrollTop;
const distance = targetScrollTop - startScrollTop;
const duration = 300; // 动画时长300ms
let startTime = null;
function animate(currentTime) {
if (!startTime) startTime = currentTime;
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
// 使用缓动函数让动画更自然
const easeProgress = easeOutCubic(progress);
scrollContainer.scrollTop = startScrollTop + distance * easeProgress;
if (progress < 1) {
requestAnimationFrame(animate);
} else {
isAnimating = false;
}
}
// 缓动函数:三次方缓出
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
requestAnimationFrame(animate);
}
兼容与优化说明
上述代码已经同时兼容了移动端触摸滑动和PC端鼠标滚轮操作,并且通过isAnimating标志位避免了动画执行期间的重复触发问题。如果需要适配更多场景,还可以添加以下优化:
- 监听窗口 resize 事件,当屏幕高度变化时重新计算
itemHeight的值,更新每个视频项的高度 - 给视频添加播放控制逻辑,切换到当前视频时自动播放,离开时暂停,减少性能消耗
- 如果视频数量较多,可以结合虚拟列表的思路,只渲染可视区域附近的视频项,提升渲染性能
常见问题排查
在实际开发中可能会遇到以下问题:
- 吸附位置不准确:检查每个视频项的高度是否和
itemHeight一致,是否存在 padding 或者 margin 影响高度计算 - 滑动触发不灵敏:可以调整滑动阈值
threshold的大小,根据产品需求设置合适的触发范围 - 动画卡顿:可以减少动画时长,或者优化缓动函数的计算逻辑,避免复杂的运算
vertical_video_scrollauto_snapfull_screen_videoscroll_adsorption修改时间:2026-06-17 12:45:19