一、核心实现原理
这个多行人像网格的平滑走马灯(Marquee)效果,核心是「内容复制+位移动画」实现无缝循环,配合CSS遮罩做边缘融合,具体拆解为4个核心逻辑:
1. 无缝循环:双份内容 + 位移衔接
这是解决「滚动到末尾跳回开头出现卡顿空白」的核心方案:
- 在每一行内放入两组完全相同的图片卡片,首尾拼接,整行总宽度 = 单组内容宽度 × 2
- 动画仅控制整行从
translateX(0)向左移动到translateX(-50%) - 当位移达到 -50% 时,第二组内容刚好完全替代第一组的初始位置,视觉上和起点完全重合
- 此时动画无限循环重置,人眼完全感知不到跳变,最终实现「持续滚动、无空白、无顿挫」的无缝效果
2. 边缘淡出:CSS 遮罩层自然融合
左侧图片逐渐透明消失的效果,依靠 CSS mask-image(遮罩图像)实现:
- 给外层容器添加从左到右的线性渐变:
transparent → 纯黑色 - CSS遮罩的规则是:黑色区域完整显示内容,透明区域隐藏内容
- 最终效果为左侧边缘的图片从透明逐步过渡到完全显示,和左侧文字区域自然融合,不会出现生硬的切割边界
3. 流畅性能:Transform GPU 硬件加速
动画选用 transform: translateX() 而非 left、margin-left 等属性:
transform会触发浏览器 GPU 硬件加速,动画过程不会触发页面重排(Reflow)- 搭配
linear匀速动画曲线,保证滚动速度全程均匀,没有忽快忽慢的变速感,完全匹配走马灯的视觉预期
4. 错落视觉:奇偶行反向滚动
通过 nth-child(even) 选择器给偶数行设置 animation-direction: reverse,让偶数行向右滚动、奇数行向左滚动:
- 无需编写第二套动画,仅靠反向播放就能实现双向错落效果
- 多行同时滚动时视觉层次更丰富,避免单方向滚动的单调感
二、分步实现步骤
按照「结构搭建 → 基础样式 → 核心动画 → 细节优化」的顺序,5步即可完整实现。
步骤1:搭建 HTML 骨架
核心规则:外层容器包裹多行轨道,每行内部放置两组完全相同的卡片。
<!-- 最外层容器:负责裁剪溢出内容、添加边缘遮罩 -->
<div class="avatar-marquee">
<!-- 第一行滚动轨道 -->
<div class="marquee-row">
<!-- 第一组:原始图片内容 -->
<div class="avatar-card"><img src="图1.jpg" alt=""></div>
<div class="avatar-card"><img src="图2.jpg" alt=""></div>
<div class="avatar-card"><img src="图3.jpg" alt=""></div>
<!-- 第二组:和上方完全一致,用于无缝衔接 -->
<div class="avatar-card"><img src="图1.jpg" alt=""></div>
<div class="avatar-card"><img src="图2.jpg" alt=""></div>
<div class="avatar-card"><img src="图3.jpg" alt=""></div>
</div>
<!-- 第二行、第三行... 结构完全一致 -->
<div class="marquee-row">
<!-- 同样放置两组相同的图片 -->
</div>
</div>
关键要求:第二组必须和第一组的图片、顺序、样式完全一致,否则衔接处会出现明显跳变。
步骤2:设置外层容器基础样式
容器的核心作用是「隐藏溢出内容 + 实现左侧渐变淡出」。
.avatar-marquee {
width: 100%; /* 占满父容器宽度 */
overflow: hidden; /* 核心:超出容器的内容全部隐藏,形成滚动视口 */
/* 左侧渐变淡出遮罩,兼容webkit内核浏览器 */
-webkit-mask-image: linear-gradient(to right, transparent 0%, #000 30%);
mask-image: linear-gradient(to right, transparent 0%, #000 30%);
}
transparent 0%, #000 30%表示:最左侧完全透明,0~30% 区域逐渐过渡为完全显示,30% 往右的区域完整展示图片。可调整百分比控制渐变过渡的宽度。
步骤3:完成行与卡片的布局
让行内卡片横向排列不换行,卡片尺寸固定不被压缩。
/* 每一行滚动轨道 */
.marquee-row {
display: flex; /* 卡片横向排列 */
gap: 16px; /* 卡片之间的间距 */
margin-bottom: 16px; /* 行与行的垂直间距 */
width: max-content; /* 核心:宽度由内容撑开,强制不换行、不折行 */
}
/* 人像卡片 */
.avatar-card {
flex-shrink: 0; /* 核心:禁止卡片被压缩,保证所有卡片尺寸一致 */
width: 320px; /* 卡片宽度,可按需调整 */
height: 220px; /* 卡片高度,可按需调整 */
border-radius: 16px;
overflow: hidden;
border: 2px solid rgba(180, 160, 255, 0.3); /* 示例中的浅紫色描边效果 */
}
.avatar-card img {
width: 100%;
height: 100%;
object-fit: cover; /* 图片填满卡片,不变形裁切 */
display: block;
}
步骤4:添加核心滚动动画
定义位移动画,并绑定到每一行,实现无限平滑滚动。
/* 定义滚动动画:从0位移到-50% */
@keyframes marqueeMove {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
/* 给行元素绑定动画 */
.marquee-row {
/* 动画名称 时长 匀速曲线 无限循环 */
animation: marqueeMove 30s linear infinite;
}
- 时长
30s控制滚动速度:数值越大,滚动越慢;数值越小,滚动越快。 - 必须使用
linear匀速曲线,不能用默认的ease,否则开头结尾会减速,出现顿挫感。
步骤5:细节优化与交互增强
补充两个体验优化点,和示例效果完全对齐。
- 偶数行反向滚动
.marquee-row:nth-child(even) {
animation-direction: reverse; /* 反向播放动画,实现向右滚动 */
}
- 鼠标悬停暂停滚动
.avatar-marquee:hover .marquee-row {
animation-play-state: paused; /* 暂停动画 */
}
鼠标移入图片区域时滚动停止,方便用户查看具体的数字人形象,移出后继续滚动,是这类展示组件的标准交互。
三、实现注意事项
- 内容宽度要求:单组图片的总宽度必须大于容器宽度,否则即使复制两份也会出现空白,无法实现无缝效果。
- 性能优化:图片数量较多时,建议给图片添加懒加载,降低首屏加载压力。
- 浏览器兼容:
mask-image在 Safari 等 WebKit 内核浏览器中需要加-webkit-前缀,上述代码已包含兼容写法。 - 速度一致性:如果每行的卡片数量不同,需要单独设置不同的动画时长,才能保证视觉上滚动速度一致。
四、完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<style>
/* 滚动容器 */
.avatar-marquee {
width: 100%;
overflow: hidden;
/* 左侧渐变遮罩,实现淡入淡出 */
-webkit-mask-image: linear-gradient(to right, transparent 0%, #000 30%);
mask-image: linear-gradient(to right, transparent 0%, #000 30%);
}
/* 每一行滚动轨道 */
.marquee-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
width: max-content;
animation: marqueeMove 30s linear infinite;
}
/* 偶数行反向滚动,错落感更强 */
.marquee-row:nth-child(even) {
animation-direction: reverse;
}
/* 图片卡片 */
.avatar-card {
flex-shrink: 0;
width: 320px;
height: 220px;
border-radius: 16px;
overflow: hidden;
border: 2px solid rgba(180, 160, 255, 0.3);
}
.avatar-card img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
/* 滚动动画:移动50%实现无缝循环 */
@keyframes marqueeMove {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
/* 鼠标悬停暂停 */
.avatar-marquee:hover .marquee-row {
animation-play-state: paused;
}
</style>
</head>
<body>
<div class="avatar-marquee">
<!-- 第一行 -->
<div class="marquee-row">
<!-- 第一组图片 -->
<div class="avatar-card"><img src="图片1地址" alt=""></div>
<div class="avatar-card"><img src="图片2地址" alt=""></div>
<div class="avatar-card"><img src="图片3地址" alt=""></div>
<div class="avatar-card"><img src="图片4地址" alt=""></div>
<!-- 第二组完全相同的图片,保证无缝衔接 -->
<div class="avatar-card"><img src="图片1地址" alt=""></div>
<div class="avatar-card"><img src="图片2地址" alt=""></div>
<div class="avatar-card"><img src="图片3地址" alt=""></div>
<div class="avatar-card"><img src="图片4地址" alt=""></div>
</div>
<!-- 第二行 -->
<div class="marquee-row">
<div class="avatar-card"><img src="图片5地址" alt=""></div>
<div class="avatar-card"><img src="图片6地址" alt=""></div>
<div class="avatar-card"><img src="图片7地址" alt=""></div>
<div class="avatar-card"><img src="图片8地址" alt=""></div>
<div class="avatar-card"><img src="图片5地址" alt=""></div>
<div class="avatar-card"><img src="图片6地址" alt=""></div>
<div class="avatar-card"><img src="图片7地址" alt=""></div>
<div class="avatar-card"><img src="图片8地址" alt=""></div>
</div>
<!-- 第三行 -->
<div class="marquee-row">
<div class="avatar-card"><img src="图片9地址" alt=""></div>
<div class="avatar-card"><img src="图片10地址" alt=""></div>
<div class="avatar-card"><img src="图片11地址" alt=""></div>
<div class="avatar-card"><img src="图片12地址" alt=""></div>
<div class="avatar-card"><img src="图片9地址" alt=""></div>
<div class="avatar-card"><img src="图片10地址" alt=""></div>
<div class="avatar-card"><img src="图片11地址" alt=""></div>
<div class="avatar-card"><img src="图片12地址" alt=""></div>
</div>
</div>
</body>
</html>
