CSS滚动驱动动画到底怎么玩,一个轮子就能让你看明白?

3,541字
15–22 分钟
in

做前端的小伙伴肯定都遇到过这种需求:页面滚动到某个位置,某个元素就“嗖”一下弹出来,或者图片滑动到视野中间就变大变清晰。以前要实现这效果,咱得搬出JavaScript的Intersection Observer,写一堆监听、回调,还得操心性能。但现在CSS直接给咱来了个王炸——animation-timeline属性配合view()函数,让动画直接跟元素的“露脸程度”挂钩,这就好比给动画装上了“眼力见儿”,它自个儿就知道什么时候该动、什么时候该歇。

目录

聊聊view()是个什么神仙操作

简单讲,view()函数就像给元素装了个“可视区域感应器”。它能返回一个时间线,这个时间线不是按秒走的,而是按照元素在滚动容器里露出多少来算的。比如说,一个图片刚开始在滚动条外面,完全看不见,它可能处于动画的“未开始”状态;当它一点点滑进来,动画就跟着进度走;等它完全滑出去,动画也就结束了。

这玩意儿简直就是CSS版的Intersection Observer,只不过咱们不用写一行JS。想象一下,一个轮播图里一排图片,滑到中间的图片自动放大变清晰,两边的图片就缩小加模糊,这种效果用view()做起来,就跟写普通动画一样顺手。

手把手教你给轮子装上动画

光说不练假把式,咱直接开干,从零搭一个“滑到中间就放大变清晰”的图片轮播。

先把轮子的架子搭好

首先得有个能滚动的容器,咱就叫它.carousel,这里面放一堆图片卡片。

<main class="carousel">
  <div class="carousel-slide">
    <img src="pic-1.jpg" alt="第一张风景图">
  </div>
  <div class="carousel-slide">
    <img src="pic-2.jpg" alt="第二张风景图">
  </div>
  <div class="carousel-slide">
    <img src="pic-3.jpg" alt="第三张风景图">
  </div>
  <div class="carousel-slide">
    <img src="pic-4.jpg" alt="第四张风景图">
  </div>
</main>

为了让这些卡片乖乖排成一行,并且超出部分能滚动,咱得给.carousel加上flex布局和溢出滚动。

.carousel {
  display: flex;
  width: max(480px, 50vw);  /* 最小宽度480px,喜欢用视口一半 */
  overflow-x: auto;          /* 横向滚动 */
}

卡片呢,咱让它固定宽度,占容器宽度的三分之一,这样一次能瞅见三个。同时为了防止被压缩,flex-shrink: 0得安排上。

.carousel .carousel-slide {
  flex-shrink: 0;
  width: calc(100% / 3);    /* 三个卡片并排 */
  aspect-ratio: 0.8;        /* 高宽比,让卡片有点形状 */
}

.carousel .carousel-slide img {
  width: 100%;              /* 图片填满卡片 */
  display: block;           /* 去掉图片底下那点空白 */
}

现在架子算是有了,但滚动起来有点生硬,咱可以加个滚动吸附,让每次滑动停下来时,卡片正好对准中间。

.carousel {
  scroll-snap-type: x mandatory;  /* 强制横向吸附 */
  scroll-behavior: smooth;        /* 滚动起来丝滑一点 */
}

.carousel .carousel-slide {
  scroll-snap-align: center;      /* 卡片吸附在容器中间 */
}

这样一来,每次滑动松开,卡片就会“咔哒”一下,稳稳停在视野中央。不过这里有个小细节,如果想把滚动条藏起来,可以加个scrollbar-width: none,但得留神,火狐和Safari处理方式不一样,咱就图个简洁,不加也完全没问题。

让轮子能转起来

接下来是关键,得让卡片在滑到中间时变个样。咱先写个动画,从模糊缩小状态,变成清晰正常大小。

@keyframes slide {
  /* 前45%和最后阶段保持模糊缩小状态 */
  45%, 100% {
    transform: scale(0.5);
    filter: blur(6px) brightness(0.8);
    border-radius: 20px;
  }
  /* 中间50%的位置清晰放大 */
  50% {
    transform: scale(1);
    filter: none;
    border-radius: 4px;
  }
}

这动画看着眼熟吧?就是平时写的那种。接下来把这动画绑定到每个卡片上,但咱得让动画按“露脸程度”跑,而不是按时间跑。

.carousel .carousel-slide {
  animation: slide;                    /* 引用动画 */
  animation-timeline: view(inline);    /* 使用视图时间线,inline表示横向滚动 */
}

就这么两行,动画就跑起来了。注意animation-timeline最好写在animation之后,不然可能被animation的默认值auto覆盖掉,导致时间线不生效。这时候去滚动轮播,就能看到卡片滑到中间时变大变清晰,滑到两边就变小变模糊,完全不用JS干预。

解决动画错位的尴尬

不过细心的可能会发现,有时候动画跑得不太对劲,比如卡片还没完全到中间就已经开始变化了,或者离开后变化太慢。这是因为view()默认的追踪范围是整个滚动视口,从元素刚露头就开始,直到完全离开才结束。

这时候可以调整view()函数里的inset参数。它就像两个“哨兵”,一个守在滚动视口起始边,一个守在结束边,用来划定动画触发的范围。比如想让动画从元素刚接触到视口下边缘开始,直到完全进入视口结束,可以这么写:

.carousel .carousel-slide {
  animation: slide;
  animation-timeline: view(100% 0%);
}

这里的100%指的是元素从视口下方边缘(即100%露出)开始触发,0%指的是到视口上方边缘(即0%露出)结束,但实际跑起来效果正好反过来——咱想要的是元素从进入视口到离开的过程中,中间阶段才做变化,所以这个值得根据效果微调。如果看不懂,可以记住一个经验:先跑起来,再调百分比,直到动画时机对味儿。

animation-range 让控制更精准

除了inset参数,还有个叫animation-range的属性也能帮咱精确控制动画区间。比如只想让动画在元素进入视口的时候触发,可以写:

.carousel .carousel-slide {
  animation: slide;
  animation-timeline: view();
  animation-range: entry;   /* 只在进入过程中播放 */
}

entry表示元素从开始进入视口到完全进入这段时间,exit则是离开的时候,cover是从开始进入直到完全离开的整个过程,contain是元素完全在视口内的时间段。这玩意儿就像给动画画了个“播放时间线”,让控制更顺手。

参数值行为描述
entry元素进入时
exit元素离开时
cover全程播放
contain完全可见时

换种玩法,背景动起来也超带感

既然view()能让任何CSS属性跟着滚动走,那咱就可以玩点花的。比如轮播图滑到中间时,背景颜色渐变一下,或者背景图片位置来个位移。

@keyframes bgMove {
  0%, 100% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
}

.carousel .carousel-slide {
  background-image: linear-gradient(45deg, #f06, #ff0);
  background-size: 200% 200%;
  animation: bgMove;
  animation-timeline: view(inline);
}

这样每个卡片滑到中间,背景颜色就像波浪一样流动起来,视觉冲击力直接拉满。而且因为动画是挂载在每个卡片上的,每个卡片滚动时都会触发自己的背景移动,完全不会互相干扰。

最后唠叨一句,view()scroll()的区别得拎清楚。scroll()是跟着滚动容器的滚动进度走,比如页面滚动到一半,动画就走到一半;而view()是跟着元素自身的可见性走,更适合做逐个元素的入场、强调效果。选哪个,就看具体场景,想全局联动就用scroll(),想单个元素“显眼包”就用view()

CSS这波操作属实把动画门槛拉低了,不用再写那些又臭又长的监听逻辑,几行样式就能搞定以前需要JS库才能实现的效果。快动手试试,给自家轮播图加点丝滑的动态,让用户滑动的时候能眼前一亮。