CSS Tips:跑马灯效果

在 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将内容沿 xy 轴平移。当然,这个过程离不开 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-sizemax-inline-size 是 CSS 中的逻辑属性,它与CSS 的书写模式有着紧密的关联:

  • 如果书写方式是从左到右(ltr),或从右到左(rtl),那么 block-size 等同于物理属性 heightmax-inline-size 等同于物理属性 max-width

  • 如果书写方式是从上到下,那么 block-size 等同于物理属性 widthmax-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 开发者来说,都不是件容易的事情,需要知道 translateinset-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 ,或者想进一步了解和掌握 CSS 相关的知识,请关注我的专栏,或者移步阅读下面这些系列教程:

相关推荐
小白学习日记12 分钟前
【复习】HTML常用标签<table>
前端·html
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀1 小时前
CSS——属性值计算
前端·css
xgq1 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
永远不打烊1 小时前
librtmp 原生API做直播推流
前端
无咎.lsy2 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec2 小时前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec2 小时前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
豆豆2 小时前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建