在 Web 上,存在着众多借鉴跑马灯理念的无限滚动展示方案,它们能在屏幕或是限定区域内循环往复地呈现信息内容。不论是作为网站首页上吸睛夺目的焦点图轮播,还是新闻板块中连绵不断的滚动新闻标题,这类无限滚动特效无疑为 Web 页面注入了生动活泼的气息,有效地抓住了用户的目光焦点。今天,我们将探讨如何仅运用 CSS 技术来实现这一类无限滚动效果,深入剖析其背后原理,并指导你如何在 Web 项目中应用它们。 跑马灯效果
跑马灯效果是一种常见的 UI 设计模式,其特点是内容以一定速度水平或垂直滚动,循环播放,给用户带来视觉上的流畅感和动感。跑马灯通常用于网站首页的焦点图轮播、新闻页面的滚动标题以及展示特定信息的小工具等场景。 在 Web 刚诞生时,HTML 提供了一个 <marquee> 元素,用于创建跑马灯效果。使用该标签,可以很容易地在 Web 中创建水平或垂直滚动的文本或图像。例如:
<marquee behavior="scroll" direction="left">
<span>#HTML</span>
<span>#CSS</span>
<span>#JS</span>
<span>#SSG</span>
<span>#webdev</span>
<span>#animation</span>
<span>#UI/UX</span>
</marquee>
上述代码将在 Web 页面中创建一个向左滚动的跑马灯效果:
添加图片注释,不超过 140 字(可选)
然而,<marquee> 标签已被 HTML5 废弃,不再推荐使用,因为它被认为是一种不够语义化的实现方式,而且不够灵活,并且在各种浏览器中的支持也不一致。MDN 也对此发出了严厉警告: 已弃用: 不再推荐使用该特性。虽然一些浏览器仍然支持它,但也许已从相关的 Web 标准中移除,也许正准备移除或出于兼容性而保留。请尽量不要使用该特性,并更新现有的代码。请注意,该特性随时可能无法正常工作。 为了替代 <marquee> 标签,现代 Web 开发通常使用 CSS 和 JavaScript 来创建跑马灯效果。CSS 可以通过动画和过渡效果来实现内容的滚动和切换,而 JavaScript 可以用于处理用户交互和动态控制跑马灯的行为。这种方式更加灵活、可控,也更加符合现代 Web 开发的规范和标准。 甚至,我们仅通过 CSS ,不依赖任何 JavaScript 脚本就可以实现类似 <marquee> 提供的跑马灯效果,而且利用许多不同的 CSS 特性,可以通过多种不同的方式来完成。 在具体介绍这些方法之前,先用下面这张草图来介绍一下我们需要的“跑马灯效果”:
添加图片注释,不超过 140 字(可选)
简单地说,我们有一个容器(如上图中蓝色矩形框),其里面包含了一系列元素(它可以是文本、图像、卡片,甚至任何你想要的元素和内容)无限滚动,而且不会结束。换句话说,当最后一个元素滑入容器时,我们希望系列中的第一个元素直接跟随它进入一个无限循环:
添加图片注释,不超过 140 字(可选)
是的,就是上图这样的一个效果。 制作跑马灯常碰到的问题 使用 CSS 制作跑马炮效果,其原理非常简单,就是使用 CSS 的 transform 的 translate() 函数或单个变换 translate 将内容沿 x 或 y 轴平移。当然,这个过程离不开 CSS 动画。例如:
<div class="marquee--container">
<div class="marquee">marquee content</div>
</div>
.marquee--container {
inline-size: 30vw;
overflow-x: hidden;
display: flex;
}
.marquee {
flex-shrink: 0;
width: fit-content;
animation: marquee 10s linear infinite;
&:hover {
animation-play-state: paused;
}
}
@keyframes marquee {
from {
translate: 100% 0%;
}
to {
translate: -100% 0%;
}
}
添加图片注释,不超过 140 字(可选)
正如呈现给你的效果,存在着很多不足。高强度的依赖父容器的固定宽度,或者有足够的元素使容器溢出,以实现平滑循环。即便是如此,往往只有一个内容,还是无法做到无限循环的滚动效果。这个时候,通常的解法是增加相同内容项:
<div class="marquee--container">
<div class="marquee">marquee content1</div>
<div class="marquee">marquee content2</div>
<div class="marquee">marquee content3</div>
</div>
.marquee {
flex-shrink: 0;
background-color: #09f;
width: fit-content;
animation: marquee 6s linear infinite;
}
@keyframes marquee {
from {
translate: 0%;
}
to {
translate: -100%;
}
}
添加图片注释,不超过 140 字(可选)
有所改善,但仔细观察,在衔接处会有一个明显的闪跳。那么如何才能使用 纯 CSS 实现一个完美的跑马灯效果呢?请继续往下阅读! 纯 CSS 制作跑马灯 跑马灯效果的关键之一是创造出重复的幻觉,其主要思想是无限循环播放跑马灯动画,实现无缝重启。因此,在第一个方案中,我采用了镜像文本的方法,即在不同的元素中内置相同的内容:
<div class="marquee__container">
<div class="marquee" aria-hidden="true">
<span>CSS is awesome</span>
<span>CSS is awesome</span>
<span>CSS is awesome</span>
<span>CSS is awesome</span>
</div>
</div>
核心的 CSS 代码如下:
@layer demo {
@keyframes marquee {
0% {
transform: translate3d(var(--move-initial), 0, 0);
}
100% {
transform: translate3d(var(--move-final), 0, 0);
}
}
.marquee__container {
position: relative;
overflow: hidden;
mask: linear-gradient(90deg, #0000, #000 5% 95%, #0000);
--offset: 20vw;
--move-initial: calc(-25% + var(--offset));
--move-final: calc(-50% + var(--offset));
}
.marquee {
width: fit-content;
display: flex;
position: relative;
transform: translate3d(var(--move-initial), 0, 0);
animation: marquee 16s linear infinite running;
.marquee__container:hover & {
animation-play-state: paused;
}
}
}
为了实现跑马灯的偏移效果(即使我们想要在开始时显示第一个项目,但被裁剪掉了),它基本上需要被拉回。因此,让我们使用四个重复的项目,即示例代码中的四个 <span> 元素。 请注意示例中的 --offset、--move-initial 和 --move-final 三个变量。--offset 设置的是一个偏移量,通过调整这个偏移量,我们可以看到一些文本,否则第一个项目会因为 --move-initial 变量的作用而被拉出容器,变得完全不可见:
添加图片注释,不超过 140 字(可选)
请注意,--move-initial 的值为 -25%,使用 translate3d(var(--move-initial), 0 , 0) 将会产生如上图所示的效果。这里之所以设置为 25%,是因为我们有四个 <span> 元素,负值在这里表示位移的方向。结合偏移变量 calc(-25% + var(--offset)),--move-initial 会根据 --offset 的不同值改变项目在容器中的位置:
添加图片注释,不超过 140 字(可选)
--move-final是动画的结束位置,在那里我们可以无缝地开始一个新的循环。它是一半的路径(现在有两个项目),再次有一个项目在左边被切掉相同的量。
添加图片注释,不超过 140 字(可选)
同样的,调整--offset可以改变--move-final的值,也就调整了动画的结束位置:
添加图片注释,不超过 140 字(可选)
在此基础上,通过给文本设置合适的字号(以 vw 为单位),我们可以确保在视口中可见三次重复。这对于“幻觉”起作用很重要(即开始下一个循环)。
你可以调整动画运动方向,从而得到两个不同方向运动的跑马灯效果:
.marquee__container:nth-child(odd) .marquee{
animation-direction: reverse;
}
添加图片注释,不超过 140 字(可选)
注意,这始终是一个“幻觉”。事实上,你始终只能看到第二个和第三个<span>的内容,第一个和第四个始终展示不全。尝试着将上面的示例中的文本标上号,所存在的问题立即就能显现:
添加图片注释,不超过 140 字(可选)
这意味着,如果跑马灯中的每一项内容不一样,上面这个方案就行不通了。欲知何解,请继续往下阅读。
把上面示例的 HTML 结构调整成下面这样:
<div class="marquee__container">
<div class="marquee" aria-hidden="true">
<span>#CSS</span>
<span>#HTML</span>
<span>#JavaScript</span>
<span>#SVG</span>
<span>#React</span>
<span>#Vue</span>
</div>
<!-- 复制一个 marueee -->
<div class="marquee">
<span>#CSS</span>
<span>#HTML</span>
<span>#JavaScript</span>
<span>#SVG</span>
<span>#React</span>
<span>#Vue</span>
</div>
</div>
代码每个项的内容是不一样的,并且完全复制了一个.marquee。其核心 CSS 代码如下:
@layer demo {
@keyframes marquee {
to {
translate: calc(-100% - var(--gap)) 0;
}
}
.marquee__container {
--gap: 1rem;
position: relative;
display: flex;
overflow: hidden;
gap: var(--gap);
mask: linear-gradient(90deg, #0000, #000 5% 95%, #0000);
}
.marquee {
flex-shrink: 0;
display: flex;
justify-content: space-around;
gap: var(--gap);
min-width: 100%;
animation: marquee 16s linear infinite running;
.marquee__container:hover & {
animation-play-state: paused;
}
}
}
简单的解释一下上面的 CSS 代码。 我们在 .marquee__container 和 .marquee 容器上使用 display: flex ,将所有项目(<span>)放在单行上,而且不会换行显式。同时在 .marquee__container 容器上设置 overflow: hidden ,将溢出的项目隐藏。可以尝试着在浏览器开发者工具中关闭 overflow: hidden ,你将看到的效果如下:
添加图片注释,不超过 140 字(可选)
当动画循环时,溢出会隐藏元素,使其回到起始位置。
在 .marquee 容器上设置 flex-shrink: 0; 是为了防止该容器收缩,避免内容重叠:
添加图片注释,不超过 140 字(可选)
.marquee 上的 min-width: 100% 比较好理解,它将每个子容器拉伸到父容器的宽度。通过这条规则,第一个子容器可见,而重复的容器(第二个 .marquee)在溢出中隐藏。 .marquee 上的 justify-content: space-around 也比较好理解,它将均匀分布每个子容器项目之间的空间,然后在第一个项目之前和最后一个项目之后应用一半的空白。你可以尝试着删除 .marquee 中的 <span> 元素,查看它的变化:
添加图片注释,不超过 140 字(可选)
上图显示的是.marquee中只有两个span项目的情景。开启动画,它的效果如下:
添加图片注释,不超过 140 字(可选)
正如你所看到的,这样做仅仅是增加了项目之间的间距,但并不影响最终的效果。 为了能更好的设置每个项目之间的间隙,并且使父容器(两个 .marquee )和子容器(span)之间的间隙一样,我们使用 CSS 的自定义属性来处理,都将它们的 gap 设置为 var(--gap) 。 无限循环的视觉是通过将第一个子容器完全移出溢出,同时将重复的容器完全拉入视图中来实现的。
@keyframes marquee {
to {
translate: calc(-100% - var(--gap)) 0;
}
}
当动画重新开始时,第一个容器从上一次结束的位置继续。幻觉完成!
添加图片注释,不超过 140 字(可选)
还可以将.marquee__container容器的max-width设置为fit-content,这样做的优势是根据内容大小调整父容器的尺寸。请注意,父容器的宽度等于两个内容容器(.marquee)的宽度,这两个容器会拉伸以填充父容器:
.marquee__container {
max-width: fit-content;
}
添加图片注释,不超过 140 字(可选)
上面示例效果中,每个项目之间间隙要比我们所设置的--gap大得多,为此,我们可以通过下面这个方式来对它进行优化:
@layer demo {
@keyframes marquee {
to {
translate: calc(-100% - var(--gap)) 0;
}
}
@keyframes marquee2 {
from {
translate: calc(100% + var(--gap));
}
to {
translate: 0;
}
}
.marquee__container {
--gap: 1rem;
position: relative;
display: flex;
overflow: hidden;
user-select: none;
gap: var(--gap);
mask: linear-gradient(90deg, #0000, #000 5% 95%, #0000);
max-width: fit-content;
}
.marquee {
flex-shrink: 0;
display: flex;
justify-content: space-around;
gap: var(--gap);
min-width: 100%;
animation: marquee 16s linear infinite running;
&:last-child {
position: absolute;
top: 0;
left: 0;
animation-name: marquee2;
}
.marquee__container:hover & {
animation-play-state: paused;
}
}
}
我们对第二个.marquee进行了绝对定位,并且两个.marquee应用了不同的关键帧动画。第一个是从容器.marquee__container最左而边缘往左平移(移出容器),第二个.marquee则从容器最右侧开始往容器里平移:
添加图片注释,不超过 140 字(可选)
这个效果优雅多了吧。你同样可以调整动画播放方向,得到一个从左往右滚动的效果:
.marquee__container:nth-child(2n) .marquee{
animation-direction: reverse;
}
添加图片注释,不超过 140 字(可选)
在这个示例中,应用了一些 CSS 的基本特性,理解这些 CSS 特性更易于帮助你更好的实现跑马灯效果:
在镜像内容的时候,我们可以减少一个嵌套容器。例如:
<div class="marquee__container">
<div class="marquee">
<span>#CSS</span>
<span>#HTML</span>
<span>#JavaScript</span>
<span>#SVG</span>
<span>#React</span>
<span>#Vue</span>
<span aria-hidden="true">#CSS</span>
<span aria-hidden="true">#HTML</span>
<span aria-hidden="true">#JavaScript</span>
<span aria-hidden="true">#SVG</span>
<span aria-hidden="true">#React</span>
<span aria-hidden="true">#Vue</span>
</div>
</div>
核心 CSS 代码:
.marquee__container {
max-width: 90vh;
overflow: hidden;
mask: linear-gradient(
90deg,
transparent,
white 20%,
white 80%,
transparent
);
.marquee {
--gap: 1rem;
padding-block: 1rem;
display: flex;
gap: var(--gap);
width: max-content;
flex-wrap: nowrap;
animation: marquee var(--_animation-duration, 20s) var(--_animation-direction, forwards) linear infinite;
}
}
@keyframes marquee {
to {
translate: calc(-50% - var(--gap) / 2);
}
}
.marquee__container:nth-child(2n) .marquee {
--_animation-direction: reverse;
}
注意,这个示例中的动画,我们使用translate沿着x向容器外平移.marquee容器内容和间距总长的一半。
添加图片注释,不超过 140 字(可选)
如果你不希望人肉复制镜像的元素,那么可以考虑使用 JavaScript 来处理:
<div class="marquee__container">
<div class="marquee">
<span>#CSS</span>
<span>#HTML</span>
<span>#JavaScript</span>
<span>#SVG</span>
<span>#React</span>
<span>#Vue</span>
</div>
</div>
const marqueeContainers = document.querySelectorAll(".marquee__container");
const addAnimation = () => {
marqueeContainers.forEach((marqueeContainer) => {
const marquee = marqueeContainer.querySelector(".marquee");
const marqueeContent = Array.from(marquee.children);
marqueeContent.forEach((item) => {
const duplicatedItem = item.cloneNode(true);
duplicatedItem.setAttribute("aria-hidden", true);
marquee.appendChild(duplicatedItem);
});
});
};
addAnimation();
添加图片注释,不超过 140 字(可选)
接下来,我们来看另外一种方案,这种方案不需要镜像元素,但面要一些计算。这种方案的 HTML 结构比较简洁:
<div class="marquee__container">
<ul class="marquee">
<li style="--index: 0;">0</li>
<li style="--index: 1;">1</li>
<li style="--index: 2;">2</li>
<li style="--index: 3;">3</li>
<li style="--index: 4;">4</li>
<li style="--index: 5;">5</li>
<li style="--index: 6;">6</li>
<li style="--index: 7;">7</li>
<li style="--index: 8;">8</li>
<li style="--index: 9;">9</li>
</ul>
</div>
li的--index是对应元素的索引值,后面在样式计算时需要使用到该属性。
@layer marquee {
@keyframes slide {
100% {
translate: var(--destination-x) var(--destination-y);
}
}
.marquee__container {
--speed: 20;
--count: 10;
container-type: size;
overflow: hidden;
}
.marquee {
display: flex;
gap: 0;
width: fit-content;
li {
/* 初始位置 */
--origin-x: calc((var(--count) - var(--index)) * 100%);
--origin-y: 0;
/* 结束位置 */
--destination-x: calc((var(--index) + 1) * -100%);
--destination-y: 0;
/**
* eg.
* 第一个 li => index = 0; --count = 10
* --origin-x: calc((var(--count) - var(--index)) * 100%)
* = calc((10 - 0) * 100%) => 1000%;
* --destination-x: calc((var(--index) + 1) * -100%) = calc((0 + 1) * -100%) => -100%;
* 第二个 li => index = 1; --count = 10
* --origin-x: calc((var(--count) - var(--index)) * 100%)
* = calc((10 - 1) * 100%) => 900%;
* --destination-x: calc((var(--index) + 1) * -100%) = calc((1 + 1) * -100%) => -200%;
* 最后个 li => index = 9
* --origin-x: calc((var(--count) - var(--index)) * 100%)
* = calc((10 - 9) * 100%) => 100%;
* --destination-x: calc((var(--index) + 1) * -100%) = calc((9 + 1) * -100%) => -1000%;
*/
translate: var(--origin-x) var(--origin-y);
/* 动画持续时间 */
--duration: calc(var(--speed) * 1s);
/* 动画延迟时间 */
--delay: calc((var(--duration) / var(--count)) * (var(--index, 0) - (var(--count) * 0.5)));
/**
* eg.
* 第一个 li => index = 0; --speed = 20; --count: 10
* --delay: calc((var(--duration) / var(--count)) *(var(--index, 0) - (var(--count) * 0.5)))
* = calc((20s / 10) * (0 - (10 * 0.5)))=> -10s;
* 第2个 li => index = 1; --speed = 20; --count: 10
* --delay: calc((var(--duration) / var(--count)) *(var(--index, 0) - (var(--count) * 0.5)))
* = calc((20s / 10) * (1 - (10 * 0.5)))=> -8s;
* 最后一个 li => index = 9; --speed = 20; --count: 10
* --delay: calc((var(--duration) / var(--count)) *(var(--index, 0) - (var(--count) * 0.5)))
* = calc((20s / 10) * (9 - (10 * 0.5)))=> 8s;
*/
animation: slide var(--duration) calc(var(--delay) - (var(--count) * 0.5s)) infinite linear;
}
}
}
代码中有多个 CSS 自定义属性:
.marquee__container {
--speed: 20; /* 动画播放速度 */
--count: 10; /* 项目总数量 */
}
这两个自定义属性将会决定li中的自定义属性的值:
.marquee {
li {
/* 初始位置 */
--origin-x: calc((var(--count) - var(--index)) * 100%);
--origin-y: 0;
/* 结束位置 */
--destination-x: calc((var(--index) + 1) * -100%);
--destination-y: 0;
/* 动画持续时间 */
--duration: calc(var(--speed) * 1s);
/* 动画延迟时间 */
--delay: calc((var(--duration) / var(--count)) * (var(--index, 0) - (var(--count) * 0.5)));
}
}
其中:
- --origin-x 和 --origin-y 决定每个 li 的初始位置,它们应用在 translate 属性上,将决定项目平移的距离
- --destination-x 和 --destination-y 决定每个 li 的终点位置(动画结束时停止位置),它们用于 @keyframes 中的 translate 属性,它将决定项目要移动的目标位置
- --duration 和 --delay 就比较好理解了,每个 li 动画的持续时间和延迟时间
详细的介绍请查看代码的注释,最终你将看到的效果如下:
添加图片注释,不超过 140 字(可选)
特别声明,这个方案来源于@Jhey,他还在 Codepen 提供了两个效果,你可以尝试着拖动示例中的滑块,查看跑马灯效果的变化:
添加图片注释,不超过 140 字(可选)
添加图片注释,不超过 140 字(可选)
这个方案,只是代码看上去比较复杂,但只要你理解和掌握了 CSS 的自定义属性、CSS 变换和CSS 动画的持续时间以及延迟时间相关的知识,那么理解上面的代码很容易了。如果你对觉得自己对这些特性还不太了解,那么我建议你移步阅读下面这些教程:
- 当然,示例中我们还应用到了一些现代 Web 布局技术,比如 CSS 的 Flexbox 布局、Grid 布局,还有一些关于 CSS 容器查询、内在尺寸以及遮罩和图像特效相关的特性。 最后,还有一种方案,这种方案来自于 @Silvestar Bistrovi? 的案例:
添加图片注释,不超过 140 字(可选)
这种方案将会应用到 CSS 的自定义属性,CSS 的逻辑属性等。注意,这种方案也无需镜像元素,例如:
<div class="marquee__container">
<div class="marquee">
<img class="marquee__item" src="http://i.pravatar.cc/300?img=1" alt="" />
<img class="marquee__item" src="http://i.pravatar.cc/300?img=2" alt="" />
<img class="marquee__item" src="http://i.pravatar.cc/300?img=3" alt="" />
<img class="marquee__item" src="http://i.pravatar.cc/300?img=4" alt="" />
<img class="marquee__item" src="http://i.pravatar.cc/300?img=5" alt="" />
<img class="marquee__item" src="http://i.pravatar.cc/300?img=6" alt="" />
<img class="marquee__item" src="http://i.pravatar.cc/300?img=7" alt="" />
<img class="marquee__item" src="http://i.pravatar.cc/300?img=8" alt="" />
<img class="marquee__item" src="http://i.pravatar.cc/300?img=9" alt="" />
<img class="marquee__item" src="http://i.pravatar.cc/300?img=10" alt="" />
</div>
</div>
与前面几个方案不一样的地方是,这个方案采用的是绝对定位。因此,需要在.marquee容器上定义position: relative,这是很有必要的。顺便将基本的样式写好:
.marquee {
display: flex;
position: relative;
mask-image: linear-gradient(
to right,
hsl(0 0% 0% / 0),
hsl(0 0% 0% / 1) 20%,
hsl(0 0% 0% / 1) 80%,
hsl(0 0% 0% / 0)
);
--marquee-max-width: 90vw;
block-size: var(--marquee-item-height);
max-inline-size: var(--marquee-max-width);
}
代码中的 --marquee-item-height 是图片的高度,--marquee-max-width 是指 .marquee 容器的最大宽度。 另外,block-size 和 max-inline-size 是 CSS 中的逻辑属性,它与CSS 的书写模式有着紧密的关联:
- 如果书写方式是从左到右(ltr),或从右到左(rtl),那么 block-size 等同于物理属性 height ,max-inline-size 等同于物理属性 max-width
- 如果书写方式是从上到下,那么 block-size 等同于物理属性 width ,max-inline-size 等同于物理属性 max-height
注意,如果你对 CSS 逻辑属性和逻辑值一无所知,那么接下来的示例代码阅读起来会有一点的难度,但并不会影响你继续往下阅读。
接着给图片 .marquee__item 进行绝对定位,它允许我们将图像从文档流中取出并手动定位它们:
.marquee__item {
position: absolute;
}
这个时候,屏幕上什么都没有。这使得图像看起来完全消失了。但是它们在那里——图像直接堆叠在彼此上面。
添加图片注释,不超过 140 字(可选)
与上一个示例类似,为了能让每张图片有自己正确的位置,我们需要再增加几个自定义属性:
.marquee {
--marquee-max-width: 90vw; /* 容器最大宽度*/
--marquee-item-height: 150px; /* 图片高度 */
--marquee-item-width: 150px; /* 图片宽度 */
--marquee-item-offset: ?; /* 图片移动距离 */
}
上面代码中 --marquee-item-offset 的值是一个问号,那是因为,它的值和图片数量有关紧密关联。在这个示例中,图片的总数是 10 (--marquee-item-count: 10),因此,--marquee-item-offset 的值是:
.marquee {
--marquee-max-width: 90vw; /* 容器最大宽度*/
--marquee-item-height: 150px; /* 图片高度 */
--marquee-item-width: 150px; /* 图片宽度 */
--marquee-item-count: 10; /* 图片总数量 */
--marquee-item-offset: max(
calc(var(--marquee-item-width) * var(--marquee-item-count)),
calc(100% + var(--marquee-item-width))
); /* 图片移动距离 */
}
在设置 --marquee-item-offset 自定义属性值时应用了 CSS 比较函数中的 max() 函数,做了一个简单的判断,最终值将会在 max() 函数中取较大的值,即所有图片的总宽度(calc(var(--marquee-item) * var(--marquee-item-count)))或容器的最大宽度加上单个图像的宽度(calc(100% + var(--marquee-item-width))。这样做是防止因 .marquee 容器太大,图片空间将小于最大空间,偏移量将在容器内部,这会使图像在 .marquee 容器内可见。
@layer marquee {
.marquee {
--marquee-max-width: 90vw; /* 容器最大宽度*/
--marquee-item-height: 150px; /* 图片高度 */
--marquee-item-width: 150px; /* 图片宽度 */
--marquee-item-count: 10; /* 图片总数量 */
--marquee-item-offset: max(
calc(var(--marquee-item-width) * var(--marquee-item-count)),
calc(100% + var(--marquee-item-width))
); /* 图片移动距离 */
block-size: var(--marquee-item-height);
max-inline-size: var(--marquee-max-width);
overflow-x: hidden;
.marquee__item {
position: absolute;
inset-inline-start: var(--marquee-item-offset);
}
}
}
现在,我们已经将跑马灯图像推出容器 .marquee 之外了,接下来是要让它怎么平移进来,然后无限循环。 要使跑马灯动画效果,我们需要以下信息:
- 图像的宽度:--marquee-item-width
- 图像的高度: --marquee-item-height
- 图像的总数量:--marquee-item-count
- 图像的偏移量:--marquee-item-offset
- 动画持续时间:--marquee-duration (待未定义)
- 动画延迟时间: --marquee-delay (待未定义)
我们要制作的跑马灯动画效果是跑马灯项目(.marquee__item)从容器(.marquee)最右侧向最左侧移动,并且允许每个项目从右边进入视图,当它越过左边缘并超出跑马灯容器的视图时,该项目不可见。 现在,我们已经有了 --marquee-item-offset ,它将跑马灯项目(.marquee__item)推到了容器最右侧,初始位置:
.marquee__item {
position: absolute;
inset-inline-start: var(--marquee-item-offset);
}
现在,我们需要一个结束状态位置,与初始状态位置相对,而且这个结束状态位置在容器最左侧。因此,我们可以在@keyframes的最后一帧中定义它:
@keyframes marquee {
to {
inset-inline-start: calc(-1 * var(--marquee-item-width));
}
}
并且将已定义的marquee动画应用在跑马灯项目上:
.marquee__item {
animation: marquee linear var(--marquee-duration) var(--marquee-delay, 0s) infinite;
}
要使跑马灯项目无限动画,我们就必须定义两个 CSS 变量,一个用于动画持续时间(--marquee-duration),一个用于动画延迟时间(--marquee-delay)。动画持续时间可以是任何长度,例如24s:
.marquee {
--marquee-duration: 24s;
}
动画持续时间就没那么简单,它需要使用以下公式来计算动画延迟时间:
.marquee__item {
--marquee-delay: calc(var(--marquee-duration) / var(--marquee-item-count) * (var(--marquee-item-count) - var(--index)) * -1);
}
公式很好理解,这里就不解释了。简单说一下公式中的--index变量。这个变量对应的是跑马灯项目在 HTML 源码中的索引值,在这里我设置了从1开始索引,主要是为了能与我们熟悉的 CSS 结构选择器相匹配:
<div class="marquee__container">
<div class="marquee">
<img style="--index: 1;" class="marquee__item" src="http://i.pravatar.cc/300?img=1" alt="" />
<img style="--index: 2;" class="marquee__item" src="http://i.pravatar.cc/300?img=2" alt="" />
<img style="--index: 3;" class="marquee__item" src="http://i.pravatar.cc/300?img=3" alt="" />
<img style="--index: 4;" class="marquee__item" src="http://i.pravatar.cc/300?img=4" alt="" />
<img style="--index: 5;" class="marquee__item" src="http://i.pravatar.cc/300?img=5" alt="" />
<img style="--index: 6;" class="marquee__item" src="http://i.pravatar.cc/300?img=6" alt="" />
<img style="--index: 7;" class="marquee__item" src="http://i.pravatar.cc/300?img=7" alt="" />
<img style="--index: 8;" class="marquee__item" src="http://i.pravatar.cc/300?img=8" alt="" />
<img style="--index: 9;" class="marquee__item" src="http://i.pravatar.cc/300?img=9" alt="" />
<img style="--index: 10;" class="marquee__item" src="http://i.pravatar.cc/300?img=10" alt="" />
</div>
</div>
最后,所有核心的 CSS 代码如下:
@layer marquee {
@keyframes marquee {
to {
inset-inline-start: calc(-1 * var(--marquee-item-width));
}
}
.marquee {
--marquee-max-width: 90vw; /* 容器最大宽度*/
--marquee-item-height: 150px; /* 图片高度 */
--marquee-item-width: 150px; /* 图片宽度 */
--marquee-item-count: 10; /* 图片总数量 */
--marquee-item-offset: max(
calc(var(--marquee-item-width) * var(--marquee-item-count)),
calc(100% + var(--marquee-item-width))
); /* 图片移动距离 */
--marquee-duration: 24s;
block-size: var(--marquee-item-height);
max-inline-size: var(--marquee-max-width);
overflow-x: hidden;
.marquee__item {
--marquee-delay: calc(var(--marquee-duration) / var(--marquee-item-count) * (var(--marquee-item-count) - var(--index)) * -1);
position: absolute;
inset-inline-start: var(--marquee-item-offset);
animation: marquee linear var(--marquee-duration) var(--marquee-delay, 0s) infinite;
translate: -50%;
}
}
}
最后,我们将跑马灯项目在水平方向上平移-50%。这个小“技巧”处理了图像尺寸不均匀的情况。
添加图片注释,不超过 140 字(可选)
这种方案与上一种方案思路基本上相似的。@Jhey 采用的是 translate 属性对跑马灯项目进行平移,而 @Silvestar Bistrovi? 采用的是绝对定位加偏移属性 inset-inline-start 对跑马灯项目进行平移。但他们都有两个核心:“都基于百分比对跑马灯项目进行平移” 和“根据跑马灯项目的顺序设置适合的动画延迟时间”。而且它们的计算对于初级的 Web 开发者来说,都不是件容易的事情,需要知道 translate 和 inset-inline-start 百分比值相对谁计算,需要知道负的动画延迟时间对动画有何影响。我在这里就不展开介绍了,有兴趣的同学可以深入探讨一下。 最后再向大家呈现一个类似新闻标题滚动的跑马灯效果,这种效果在 Web 应用上经常能看到,例如:
@layer animation {
:root {
--easing: cubic-bezier(0.31, 1.28, 0.32, 1.275);
--timing: 1s;
}
@keyframes slideOutUp {
100% {
opacity: 0;
translate: 0 -100% 0;
}
}
@keyframes slideInUp {
0% {
opacity: 0;
translate: 0 100% 0;
}
}
.word {
view-transition-name: word-swap;
}
::view-transition-new(word-swap),
::view-transition-old(word-swap) {
height: 100%;
object-fit: cover;
object-position: center;
animation-timing-function: var(--easing);
animation-duration: var(--timing);
}
::view-transition-old(word-swap) {
animation-name: slideOutUp;
}
::view-transition-new(word-swap) {
animation-name: slideInUp;
}
}
这个效果你还需要使用一点 JavaScript 代码:
const words = ["沙发垫秋冬款防滑", "金丝绒加厚打底衫女", "床边晚上放衣服神器", "2023新款鸳鸯火锅"];
let counter = 1;
let interal = setInterval(() => {
if (counter >= words.length) {
counter = 0;
}
const nextWord = words[counter];
viewTransition(() => {
document.querySelector(".word").innerText = nextWord;
});
counter++;
}, 1500);
function viewTransition(fn, params) {
if (document.startViewTransition) {
document.startViewTransition(() => fn(params));
} else {
fn(params);
}
}
最终你将看到的效果如下:
添加图片注释,不超过 140 字(可选)
小结 文章中我们介绍了实现跑马灯动画效果的多种不同方案,每一种方案各有利弊,具体在实际生产中时,需要根据自己的业务需求进行选择。 最后简单的总结一下 CSS 实现跑马灯动画效果所应用到的相关技术:
- Flexbox 布局: 使用 Flexbox 布局来创建一个水平排列的容器,使得内容在一行上水平排列。
- 定位跑马灯项目: 将每个跑马灯项目(如图片或文本)设置为绝对定位,以便控制其位置和动画效果。也可以使用 CSS 变换来设置跑马灯项目的位置
- 动画效果定义: 使用 @keyframes 关键帧动画定义跑马灯的运动轨迹和效果。通常是将内容从右侧移动到左侧,使其看起来像是无限循环滚动。
- CSS 自定义属性(变量): 使用 CSS 变量来定义动画的持续时间、延迟时间和其他相关参数,以便于调整和定制动画效果。
- 容器尺寸限制: 通过设置容器的最大宽度和溢出属性来限制内容的显示范围,确保内容在容器之外不可见。
- 动态计算偏移量: 使用 calc() 函数动态计算每个跑马灯项目的偏移量,确保项目在动画开始时位于容器的右侧边缘。
- 动画延迟设置: 根据项目数量和动画持续时间,使用适当的公式计算动画延迟时间,以实现连续和流畅的跑马灯效果。
- 优化和改进: 根据实际需求进行优化和改进,例如处理不同尺寸和数量的项目、调整动画速度和间距等。
通过以上方案,可以实现具有吸引力和流畅效果的跑马灯动画,使其成为网站中吸引眼球的元素之一。