在浏览器中实现无缝循环滚动

无缝循环滚动是浏览器中一种常见的网页效果,在屏幕长宽受限的情况下,用于展现更多数据,在图片轮播、公告栏和大屏滚动等场景很常见。本文将介绍几种在浏览器中,实现的无缝循环滚动效果的思路。

循环滚动的原理

界面的循环滚动,依靠 CSS 关键帧实现,一个关键帧的循环就成了动画,这就像循环播放的 GIF 一样,中间的画面只是一次片段,而这个片段是由物体的一次运动组成。

首先,我们需要定义一个 0% 到 100% 的位移关键帧,使用 translateY 或 translateX 控制元素的位移距离。此时,这个关键帧就是一个片段,执行一次运动。

1
2
3
4
5
6
7
8
9
@keyframes scroll {
0% {
transform: translateY(0);
}

100% {
transform: translateY(500px);
}
}

然后,我们需要为元素应用关键帧动画效果,通过使用 animation 我们可以定义关键帧的一些状态,例如关键播放的速度、次数、状态和效果。

为了让关键帧无限循环,我们需要将其设置为 infinite。此外,为了让关键帧循环更加自然,可以为其设置播放效果为 linear,使其保持匀速运动,这样每次的关键帧前进的时间和距离都是相同的。

1
2
3
.element {
animation: scroll 5s linear infinite;
}

当为元素应用了动画后,关键帧就会开始循环的执行运动。这实际上是一种视觉效果,动画中的关键帧是一直在执行运动的,只是每次片段的开始和结尾都是衔接的,我们看不出区别,也就无法感知。

现在,我们理解了循环滚动的原理,动画的循环就是同一个关键帧在不停的衔接播放。

无缝循环滚动的方式

我们了解到的循环,实际上关键帧都是在可视范围内的移动的。但是,如果关键帧的运动移动到了可视范围之外,那么这个缺少的部分,就会产生一段空白,而这个空白正好是可视范围的高度或宽度。当下一个关键帧播放时,可视范围内就会出现闪烁。

可视范围空白

那么,为了使关键帧能够头尾衔接,实现无缝滚动,就需要处理这段空白。于是,我们现在需要的就是将这个空白填补上,最直接的方式就是使用头部内容填充。

头部内容填充

我们可以将关键帧相同的内容,拼接到上一个关键帧的最后。当关键帧移动到可视范围之外时,重复的内容滚动到顶部就填充了可视范围的空白。

头部内容填充

当然,这里的重复内容的数量,取决于需要多少内容才能铺满可视范围的高度或宽度。

此外,因为每个关键帧的运动速度和位移距离都是相同的,即使一个关键帧播放完毕,下一个关键帧也能够完美的重叠在滚动的布局上。

为了更好的确定重复元素的放置的位置,我们将关键帧播放的终点定在可视范围的顶部,然后将关键帧整个移动到外面。所以,就需要确定整个关键帧的长度,在浏览器中就是每个布局子项的高度总和。

例如,关键帧总共有 3 个子项目,每个子项的高度为 300px,那么关键帧的位移距离就是 900px

1
2
3
4
5
6
7
8
9
10
@keyframes scroll {
0% {
transform: translateY(0);
}

100% {
/* 完全移动到布局外的高度,3个项目 × 项目高度300px */
transform: translateY(-900px);
}
}

确定好关键帧全部移出可视范围的位移后,我们就可以为关键帧子项目添加重复的子项目了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="carousel-track">
<div class="carousel-item">
<span>第一个项目</span>
</div>
<div class="carousel-item">
<span>第二个项目</span>
</div>
<div class="carousel-item">
<span>第三个项目</span>
</div>
<!-- 重复内容,用于填充关键帧移动到布局外的空白 -->
<div class="carousel-item">
<span>第一个项目</span>
</div>
<div class="carousel-item">
<span>第二个项目</span>
</div>
<div class="carousel-item">
<span>第三个项目</span>
</div>
</div>

最后为关键帧添加无限循环的动画,调整一下播放的速度,就实现了无缝循环无限滚动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>竖向无缝循环滚动</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}

.carousel-container {
width: 400px;
height: 500px;
background: white;
border-radius: 20px;
overflow: hidden;
}

.carousel-track {
display: flex;
flex-direction: column;
animation: scroll 12s linear infinite;
}

.carousel-item {
height: 300px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}

.carousel-item:nth-child(3n+1) {
background: linear-gradient(135deg, #ff9a9e, #f6703f);
}

.carousel-item:nth-child(3n+2) {
background: linear-gradient(135deg, #ee90f5, #9a71ea);
}

.carousel-item:nth-child(3n) {
background: linear-gradient(135deg, #93fbe3, #a3d2fc);
}


@keyframes scroll {
0% {
transform: translateY(0);
}

100% {
/* 完全移动到布局外的高度,3个项目 × 项目高度300px */
transform: translateY(-900px);
}
}

/* 鼠标悬停时暂停动画 */
.carousel-container:hover .carousel-track {
animation-play-state: paused;
}
</style>
</head>

<body>
<div class="carousel-container">
<div class="carousel-track">
<div class="carousel-item">
<span>第一个项目</span>
</div>
<div class="carousel-item">
<span>第二个项目</span>
</div>
<div class="carousel-item">
<span>第三个项目</span>
</div>
<!-- 重复内容,用于填充关键帧移动到布局外的空白 -->
<div class="carousel-item">
<span>第一个项目</span>
</div>
<div class="carousel-item">
<span>第二个项目</span>
</div>
<div class="carousel-item">
<span>第三个项目</span>
</div>
</div>
</div>
</body>

</html>

但是,这种方式只适合确定了固定子项数量的场景,如果子项的数量是动态的,那么就需要动态地计算关键帧的偏移距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>竖向动态无缝循环滚动</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}

.carousel-container {
width: 400px;
height: 500px;
background: white;
border-radius: 20px;
overflow: hidden;
}

.carousel-track {
display: flex;
flex-direction: column;
}

.carousel-item {
width: 100%;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}

/* 鼠标悬停时暂停动画 */
.carousel-container:hover .carousel-track {
animation-play-state: paused;
}
</style>
</head>

<body>
<div class="carousel-container">
<div class="carousel-track" id="carouselTrack"></div>
</div>

<script>
const carouselTrack = document.getElementById('carouselTrack');
const fragment = document.createDocumentFragment();

// 随机生成3-5个项目
const itemCount = 3 + Math.floor(Math.random() * 3);

//颜色样式
const colorGradients = [
'linear-gradient(135deg, #ff9a9e, #f6703f)',
'linear-gradient(135deg, #ee90f5, #9a71ea)',
'linear-gradient(135deg, #93fbe3, #a3d2fc)',
'linear-gradient(135deg, #84fab0, #8fd3f4)',
'linear-gradient(135deg, #a6c1ee, #fbc2eb)'
];

let nthChildCSS = '';
Array.from({ length: itemCount }).forEach((_, i) => {
nthChildCSS += `
.carousel-item:nth-child(${itemCount}n+${i + 1}) {
background: ${colorGradients[i]};
}
`;
});

// 创建原始项目
Array.from({ length: itemCount }).forEach((_, i) => {
const item = document.createElement('div');
item.className = 'carousel-item';
item.innerHTML = `<span>第${i + 1}个项目</span>`;
fragment.appendChild(item);
});

// 克隆子项用于无缝滚动
Array.from({ length: itemCount }).forEach((_, i) => {
const item = document.createElement('div');
item.className = 'carousel-item';
item.innerHTML = `<span>第${i + 1}个项目</span>`;
fragment.appendChild(item);
});
// 一次性追加子项
carouselTrack.appendChild(fragment);

// 动态设置动画
const totalHeight = itemCount * 300; // 每个项目高度是300px
const animationDuration = itemCount * 4; // 每个项目4秒

// 动态创建CSS样式
const style = document.createElement('style');

style.innerHTML = `
.carousel-track {
animation: scroll ${animationDuration}s linear infinite;
}

@keyframes scroll {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-${totalHeight}px);
}
}

${nthChildCSS}
`;
document.head.appendChild(style);
</script>
</body>

</html>

需要注意的是,子项目高度会受到内部 padding 的影响,因此需要将子项目 box-sizing 设置为 border-box,然后为其设置一个固定高度。另外,如果子项设置了 margin,那么也需要计入关键帧的偏移距离。

结语

至此,我们理解了在浏览器中的元素是如何实现无缝无限滚动的,无缝滚动只是一种视觉效果。除此之外,在浏览器中有还有很多通过视觉效果实现布局的特殊效果的场景,但是当我们了解原理后,就能很好理解了它们的实现方式。


在浏览器中实现无缝循环滚动
http://blog.itea.dev/2025/07/07/在浏览器中实现无缝循环滚动/
作者
isixe
发布于
2025年7月7日
许可协议