在 Web 上,存在着众多借鉴跑马灯理念的无限滚动展示方案,它们能在屏幕或是限定区域内循环往复地呈现信息内容。不论是作为网站首页上吸睛夺目的焦点图轮播,还是新闻板块中连绵不断的滚动新闻标题,这类无限滚动特效无疑为 Web 页面注入了生动活泼的气息,有效地抓住了用户的目光焦点。今天,我们将探讨如何仅运用 CSS 技术来实现这一类无限滚动效果,深入剖析其背后原理,并指导你如何在 Web 项目中应用它们。
跑马灯效果
跑马灯效果是一种常见的 UI 设计模式,其特点是内容以一定速度水平或垂直滚动,循环播放,给用户带来视觉上的流畅感和动感。跑马灯通常用于网站首页的焦点图轮播、新闻页面的滚动标题以及展示特定信息的小工具等场景。
在 Web 刚诞生时,HTML 提供了一个 <marquee>
元素,用于创建跑马灯效果。使用该标签,可以很容易地在 Web 中创建水平或垂直滚动的文本或图像。例如:
HTML
<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 页面中创建一个向左滚动的跑马灯效果:
Demo 地址:codepen.io/airen/full/...
然而,<marquee>
标签已被 HTML5 废弃,不再推荐使用,因为它被认为是一种不够语义化的实现方式,而且不够灵活,并且在各种浏览器中的支持也不一致。MDN 也对此发出了严厉警告:
已弃用: 不再推荐使用该特性。虽然一些浏览器仍然支持它,但也许已从相关的 Web 标准中移除,也许正准备移除或出于兼容性而保留。请尽量不要使用该特性,并更新现有的代码。请注意,该特性随时可能无法正常工作。
为了替代 <marquee>
标签,现代 Web 开发通常使用 CSS 和 JavaScript 来创建跑马灯效果。CSS 可以通过动画和过渡效果来实现内容的滚动和切换,而 JavaScript 可以用于处理用户交互和动态控制跑马灯的行为。这种方式更加灵活、可控,也更加符合现代 Web 开发的规范和标准。
甚至,我们仅通过 CSS ,不依赖任何 JavaScript 脚本就可以实现类似 <marquee>
提供的跑马灯效果,而且利用许多不同的 CSS 特性,可以通过多种不同的方式来完成。
在具体介绍这些方法之前,先用下面这张草图来介绍一下我们需要的"跑马灯效果":
简单地说,我们有一个容器(如上图中蓝色矩形框),其里面包含了一系列元素(它可以是文本、图像、卡片,甚至任何你想要的元素和内容)无限滚动,而且不会结束。换句话说,当最后一个元素滑入容器时,我们希望系列中的第一个元素直接跟随它进入一个无限循环:
是的,就是上图这样的一个效果。
制作跑马灯常碰到的问题
使用 CSS 制作跑马炮效果,其原理非常简单,就是使用 CSS 的 transform 的 translate() 函数或单个变换 translate将内容沿 x
或 y
轴平移。当然,这个过程离不开 CSS 动画。例如:
HTML
<div class="marquee--container">
<div class="marquee">marquee content</div>
</div>
CSS
.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%;
}
}
Demo 地址:codepen.io/airen/full/...
正如呈现给你的效果,存在着很多不足。高强度的依赖父容器的固定宽度,或者有足够的元素使容器溢出,以实现平滑循环。即便是如此,往往只有一个内容,还是无法做到无限循环的滚动效果。这个时候,通常的解法是增加相同内容项:
HTML
<div class="marquee--container">
<div class="marquee">marquee content1</div>
<div class="marquee">marquee content2</div>
<div class="marquee">marquee content3</div>
</div>
CSS
.marquee {
flex-shrink: 0;
background-color: #09f;
width: fit-content;
animation: marquee 6s linear infinite;
}
@keyframes marquee {
from {
translate: 0%;
}
to {
translate: -100%;
}
}
Demo 地址:codepen.io/airen/full/...
有所改善,但仔细观察,在衔接处会有一个明显的闪跳。那么如何才能使用 纯 CSS 实现一个完美的跑马灯效果呢?请继续往下阅读!
纯 CSS 制作跑马灯
跑马灯效果的关键之一是创造出重复的幻觉,其主要思想是无限循环播放跑马灯动画,实现无缝重启。因此,在第一个方案中,我采用了镜像文本的方法,即在不同的元素中内置相同的内容:
HTML
<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 代码如下:
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
变量的作用而被拉出容器,变得完全不可见:
请注意,--move-initial
的值为 -25%
,使用 translate3d(var(--move-initial), 0 , 0)
将会产生如上图所示的效果。这里之所以设置为 25%
,是因为我们有四个 <span>
元素,负值在这里表示位移的方向。结合偏移变量 calc(-25% + var(--offset))
,--move-initial
会根据 --offset
的不同值改变项目在容器中的位置:
--move-final
是动画的结束位置,在那里我们可以无缝地开始一个新的循环。它是一半的路径(现在有两个项目),再次有一个项目在左边被切掉相同的量。
同样的,调整 --offset
可以改变 --move-final
的值,也就调整了动画的结束位置:
在此基础上,通过给文本设置合适的字号(以 vw
为单位),我们可以确保在视口中可见三次重复。这对于"幻觉"起作用很重要(即开始下一个循环)。
你可以调整动画运动方向,从而得到两个不同方向运动的跑马灯效果:
CSS
.marquee__container:nth-child(odd) .marquee{
animation-direction: reverse;
}
Demo 地址:codepen.io/airen/full/...
注意,这始终是一个"幻觉"。事实上,你始终只能看到第二个和第三个 <span>
的内容,第一个和第四个始终展示不全。尝试着将上面的示例中的文本标上号,所存在的问题立即就能显现:
Demo 地址:codepen.io/airen/full/...
这意味着,如果跑马灯中的每一项内容不一样,上面这个方案就行不通了。欲知何解,请继续往下阅读。
把上面示例的 HTML 结构调整成下面这样:
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 代码如下:
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
,你将看到的效果如下:
当动画循环时,溢出会隐藏元素,使其回到起始位置。
在 .marquee
容器上设置 flex-shrink: 0;
是为了防止该容器收缩,避免内容重叠:
.marquee
上的 min-width: 100%
比较好理解,它将每个子容器拉伸到父容器的宽度。通过这条规则,第一个子容器可见,而重复的容器(第二个 .marquee
)在溢出中隐藏。
.marquee
上的 justify-content: space-around
也比较好理解,它将均匀分布每个子容器项目之间的空间,然后在第一个项目之前和最后一个项目之后应用一半的空白。你可以尝试着删除 .marquee
中的 <span>
元素,查看它的变化:
上图显示的是 .marquee
中只有两个 span
项目的情景。开启动画,它的效果如下:
正如你所看到的,这样做仅仅是增加了项目之间的间距,但并不影响最终的效果。
为了能更好的设置每个项目之间的间隙,并且使父容器(两个 .marquee
)和子容器(span
)之间的间隙一样,我们使用 CSS 的自定义属性来处理,都将它们的 gap
设置为 var(--gap)
。
无限循环的视觉是通过将第一个子容器完全移出溢出,同时将重复的容器完全拉入视图中来实现的。
CSS
@keyframes marquee {
to {
translate: calc(-100% - var(--gap)) 0;
}
}
当动画重新开始时,第一个容器从上一次结束的位置继续。幻觉完成!
Demo 地址:codepen.io/airen/full/...
还可以将 .marquee__container
容器的 max-width
设置为 fit-content
,这样做的优势是根据内容大小调整父容器的尺寸。请注意,父容器的宽度等于两个内容容器(.marquee
)的宽度,这两个容器会拉伸以填充父容器:
CSS
.marquee__container {
max-width: fit-content;
}
Demo 地址:codepen.io/airen/full/...
上面示例效果中,每个项目之间间隙要比我们所设置的 --gap
大得多,为此,我们可以通过下面这个方式来对它进行优化:
CSS
@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
则从容器最右侧开始往容器里平移:
Demo 地址:codepen.io/airen/full/...
这个效果优雅多了吧。你同样可以调整动画播放方向,得到一个从左往右滚动的效果:
CSS
.marquee__container:nth-child(2n) .marquee{
animation-direction: reverse;
}
Demo 地址:codepen.io/airen/full/...
在这个示例中,应用了一些 CSS 的基本特性,理解这些 CSS 特性更易于帮助你更好的实现跑马灯效果:
请继续。
在镜像内容的时候,我们可以减少一个嵌套容器。例如:
HTML
<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 代码:
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
容器内容和间距总长的一半。
Demo 地址:codepen.io/airen/full/...
如果你不希望人肉复制镜像的元素,那么可以考虑使用 JavaScript 来处理:
HTML
<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>
JavaScript
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();
Demo 地址:codepen.io/airen/full/...
接下来,我们来看另外一种方案,这种方案不需要镜像元素,但面要一些计算。这种方案的 HTML 结构比较简洁:
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
是对应元素的索引值,后面在样式计算时需要使用到该属性。
CSS
@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 自定义属性:
CSS
.marquee__container {
--speed: 20; /* 动画播放速度 */
--count: 10; /* 项目总数量 */
}
这两个自定义属性将会决定 li
中的自定义属性的值:
CSS
.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
动画的持续时间和延迟时间
详细的介绍请查看代码的注释,最终你将看到的效果如下:
Demo 地址:codepen.io/airen/full/...
特别声明,这个方案来源于 @Jhey ,他还在 Codepen 提供了两个效果,你可以尝试着拖动示例中的滑块,查看跑马灯效果的变化:
Demo 地址:codepen.io/jh3y/full/L...
Demo 地址:codepen.io/jh3y/full/B...
这个方案,只是代码看上去比较复杂,但只要你理解和掌握了 CSS 的自定义属性、CSS 变换和CSS 动画的持续时间以及延迟时间相关的知识,那么理解上面的代码很容易了。如果你对觉得自己对这些特性还不太了解,那么我建议你移步阅读下面这些教程:
当然,示例中我们还应用到了一些现代 Web 布局技术,比如 CSS 的 Flexbox 布局、Grid 布局,还有一些关于 CSS 容器查询、内在尺寸以及遮罩和图像特效相关的特性。
最后,还有一种方案,这种方案来自于 @Silvestar Bistrović 的案例:
Demo 地址:codepen.io/smashingmag...
这种方案将会应用到 CSS 的自定义属性,CSS 的逻辑属性等。注意,这种方案也无需镜像元素,例如:
HTML
<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
,这是很有必要的。顺便将基本的样式写好:
CSS
.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
进行绝对定位,它允许我们将图像从文档流中取出并手动定位它们:
CSS
.marquee__item {
position: absolute;
}
这个时候,屏幕上什么都没有。这使得图像看起来完全消失了。但是它们在那里------图像直接堆叠在彼此上面。
与上一个示例类似,为了能让每张图片有自己正确的位置,我们需要再增加几个自定义属性:
CSS
.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
的值是:
CSS
.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
容器内可见。
CSS
@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
)推到了容器最右侧,初始位置:
CSS
.marquee__item {
position: absolute;
inset-inline-start: var(--marquee-item-offset);
}
现在,我们需要一个结束状态位置,与初始状态位置相对,而且这个结束状态位置在容器最左侧。因此,我们可以在 @keyframes
的最后一帧中定义它:
scss
@keyframes marquee {
to {
inset-inline-start: calc(-1 * var(--marquee-item-width));
}
}
并且将已定义的 marquee
动画应用在跑马灯项目上:
CSS
.marquee__item {
animation: marquee linear var(--marquee-duration) var(--marquee-delay, 0s) infinite;
}
要使跑马灯项目无限动画,我们就必须定义两个 CSS 变量,一个用于动画持续时间(--marquee-duration
),一个用于动画延迟时间(--marquee-delay
)。动画持续时间可以是任何长度,例如 24s
:
CSS
.marquee {
--marquee-duration: 24s;
}
动画持续时间就没那么简单,它需要使用以下公式来计算动画延迟时间:
CSS
.marquee__item {
--marquee-delay: calc(var(--marquee-duration) / var(--marquee-item-count) * (var(--marquee-item-count) - var(--index)) * -1);
}
公式很好理解,这里就不解释了。简单说一下公式中的 --index
变量。这个变量对应的是跑马灯项目在 HTML 源码中的索引值,在这里我设置了从 1
开始索引,主要是为了能与我们熟悉的 CSS 结构选择器相匹配:
HTML
<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 代码如下:
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%
。这个小"技巧"处理了图像尺寸不均匀的情况。
Demo 地址: codepen.io/airen/full/...
这种方案与上一种方案思路基本上相似的。@Jhey 采用的是 translate
属性对跑马灯项目进行平移,而 @Silvestar Bistrović 采用的是绝对定位加偏移属性 inset-inline-start
对跑马灯项目进行平移。但他们都有两个核心:"都基于百分比对跑马灯项目进行平移" 和"根据跑马灯项目的顺序设置适合的动画延迟时间"。而且它们的计算对于初级的 Web 开发者来说,都不是件容易的事情,需要知道 translate
和 inset-inline-start
百分比值相对谁计算,需要知道负的动画延迟时间对动画有何影响。我在这里就不展开介绍了,有兴趣的同学可以深入探讨一下。
最后再向大家呈现一个类似新闻标题滚动的跑马灯效果,这种效果在 Web 应用上经常能看到,例如:
上图是手机淘宝的搜索框中的信息滚动效果。我们可以采用现代 CSS 中的视图过渡(CSS View Transition API)来实现。
CSS
@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 代码:
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);
}
}
最终你将看到的效果如下:
Demo 地址:codepen.io/airen/full/...
示例代码在这里就不详细剖析了,如果你对这种技术方案感兴趣,请移步阅读《Web 动画之旅》的《使用 CSS 视图过渡创建流畅的界面动效》!
小结
文章中我们介绍了实现跑马灯动画效果的多种不同方案,每一种方案各有利弊,具体在实际生产中时,需要根据自己的业务需求进行选择。
最后简单的总结一下 CSS 实现跑马灯动画效果所应用到的相关技术:
-
Flexbox 布局: 使用 Flexbox 布局来创建一个水平排列的容器,使得内容在一行上水平排列。
-
定位跑马灯项目: 将每个跑马灯项目(如图片或文本)设置为绝对定位,以便控制其位置和动画效果。也可以使用 CSS 变换来设置跑马灯项目的位置
-
动画效果定义: 使用
@keyframes
关键帧动画定义跑马灯的运动轨迹和效果。通常是将内容从右侧移动到左侧,使其看起来像是无限循环滚动。 -
CSS 自定义属性(变量): 使用 CSS 变量来定义动画的持续时间、延迟时间和其他相关参数,以便于调整和定制动画效果。
-
容器尺寸限制: 通过设置容器的最大宽度和溢出属性来限制内容的显示范围,确保内容在容器之外不可见。
-
动态计算偏移量: 使用
calc()
函数动态计算每个跑马灯项目的偏移量,确保项目在动画开始时位于容器的右侧边缘。 -
动画延迟设置: 根据项目数量和动画持续时间,使用适当的公式计算动画延迟时间,以实现连续和流畅的跑马灯效果。
-
优化和改进: 根据实际需求进行优化和改进,例如处理不同尺寸和数量的项目、调整动画速度和间距等。
通过以上方案,可以实现具有吸引力和流畅效果的跑马灯动画,使其成为网站中吸引眼球的元素之一。
如果你对 CSS 方面的技巧感兴趣,请移步阅读:
- CSS Tips:CSS 实现文本淡化效果的不同姿势
- CSS Tips:用于按钮上的小技巧
- CSS Tips:CSS 如何穿透 SVG 的 use
- CSS Tips:水波纹
- CSS Tips:边框动画
- CSS Tips:圆形文本排版
- # CSS Tips:连续动态渐变
如果你觉得该教程对你有所帮助,请给我点个赞。要是你喜欢 CSS ,或者想进一步了解和掌握 CSS 相关的知识,请关注我的专栏,或者移步阅读下面这些系列教程: