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 相关的知识,请关注我的专栏,或者移步阅读下面这些系列教程:

相关推荐
轻口味35 分钟前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami38 分钟前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
吃杠碰小鸡1 小时前
lodash常用函数
前端·javascript
emoji1111111 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼1 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250031 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235952 小时前
web复习(三)
前端
机器视觉李小白2 小时前
使用 HTML 和 CSS 实现绚丽的节日烟花效果
css·html·烟花·节日·节日祝福
AiFlutter2 小时前
Flutter-底部分享弹窗(showModalBottomSheet)
java·前端·flutter