CSS 滾動驅動動畫終於正式支援了

蓦然JL發表於2024-04-18

在最新的Chrome 115中,令人無比期待的CSS 滾動驅動動畫(CSS scroll-driven animations)終於正式支援了\~有了它,幾乎以前任何需要JS監聽滾動的互動都可以純 CSS 實現了,就是這麼強大,一起了解一下吧

溫馨提示:文章略長,建議收藏後反覆查閱

一、快速入門 CSS 滾動驅動動畫

直接介紹 API 可能不太感興趣,這裡先透過一個最直觀的例子感受一下。

下面是一個頁面進度指示器,進度隨著頁面的滾動而變化

頁面很簡單,很多內容和一個進度條

<div class="progress"></div>
...很多內容

進度條是fixed定位

.progress{
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 10px;
  background-color: #F44336;
  transform-origin: 0 50%;
}

然後給這個進度條新增一個動畫,表示進度從0100%

@keyframes grow-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

接著給這個進度條繫結動畫

.progress{
  animation: grow-progress 3s linear;
}

重新整理頁面,可以看到進度條在3s內從0增長到了100%

顯然這種動畫沒什麼意義,我們需要在滾動時才觸發,並且滾動多少,動畫就播放多少。

注意:動畫時長不能為0,因為為0表示動畫不執行,所以必須寫上一個任意非零時間,或者直接為auto

最後,加上最核心的一段,也就是今天的主角animation-timeline

.progress{
  /*...*/
  animation-timeline: scroll();
}

這樣進度條就乖乖的跟隨頁面滾動而變化了(注意Chrome 115+

是不是非常簡單?是不是非常神奇?如果你感興趣,可以接著往下看

二、CSS 滾動驅動動畫

大家可能知道,傳統 JS 監聽滾動有一些問題,如下

  • 現代瀏覽器在單獨的程序上執行滾動,因此只能非同步傳遞滾動事件。
  • 由於是非同步傳遞,因此主執行緒動畫容易出現卡頓

因此,為了解決滾動卡頓的問題,CSS 滾動驅動動畫應運而生。那麼,什麼是 CSS 滾動驅動動畫?

預設情況下,動畫是隨著時間的流逝而播放的。

CSS 滾動驅動動畫指的是將動畫的執行過程由頁面滾動進行接管,也就是這種情況下,動畫只會跟隨頁面滾動的變化而變化,也就是滾動多少,動畫就執行多少,時間不再起作用

如何改變動畫的時間線呢? 那就需要用到這個核心概念了:animation-timeline,表示動畫時間線(或者叫時間軸),用於控制 CSS 動畫進度的時間線,是必不可少的一個屬性。

CSS 滾動驅動動畫終於正式支援了

預設值是auto,也是就傳統的時間線。下面是它一些關鍵詞

/* 關鍵詞 */
animation-timeline: none;
animation-timeline: auto;
/* 命名時間線 */
animation-timeline: --timeline_name;

/* 滾動時間線 */
animation-timeline: scroll();
animation-timeline: scroll(scroller axis);

/* 檢視時間線 */
animation-timeline: view();
animation-timeline: view(axis inset);

是不是有點混亂?不要慌,實際滾動場景千千萬,這裡可以分為兩大類:一類是滾動進度時間線,也就是上面的關鍵詞scroll(),還有一類是檢視進度時間線,也就是關鍵詞view()

兩者形式對應兩種不同的應用場景,這是什麼意思呢?下面一一介紹

三. CSS 滾動進度時間線

滾動進度時間線(scroll progress timeline。表示頁面或者容器滾動,將滾動進度對映到動畫進度上。起始滾動位置代表 0% 進度,結束滾動位置代表 100% 進度,下面是一個視覺化演示

在上面的進度條例子中,我們用到的就是scroll progress timeline,因為我們監聽的就是頁面的滾動

animation-timeline: scroll();

這裡的scroll()是一個簡寫,可以傳遞兩個引數,分別是<scroller><axis>

<scroller>表示滾動容器,支援以下幾個關鍵值

  • nearest:使用最近的祖先滾動容器*(預設)*
  • root:使用文件視口作為滾動容器。
  • self:使用元素本身作為滾動容器。

<axios>表示滾動方向,支援以下幾個關鍵值

  • block:滾動容器的塊級軸方向*(預設)*。
  • inline:滾動容器內聯軸方向。
  • y:滾動容器沿 y 軸方向。
  • x:滾動容器沿 x 軸方向。
/* 無引數 */
animation-timeline: scroll();

/* 設定滾動容器 */
animation-timeline: scroll(nearest); /* 預設 */
animation-timeline: scroll(root);
animation-timeline: scroll(self);

/* 設定滾動方向 */
animation-timeline: scroll(block); /* 預設 */
animation-timeline: scroll(inline);
animation-timeline: scroll(y);
animation-timeline: scroll(x);

/* 同時設定 */
animation-timeline: scroll(block nearest); /* 預設 */
animation-timeline: scroll(inline root);
animation-timeline: scroll(x self);
需要注意的是,這裡語法容錯性比較強,沒有順序要求,會自動識別

因此,如果需要監聽橫向滾動,可以這樣

animation-timeline: scroll(inline);

不知大家發現沒,前面的滾動容器只有三個關鍵詞,並不能透過#id方式任意指定滾動容器,真的能滿足所有需求嗎?

當然不行!有時候結構稍微複雜一點,自動查詢就不適用了,並且這裡的最近祖先滾動容器還受到絕對定位的影響,因此,我們還需要手動去指定滾動容器。

官方的解決方式是建立一個帶有名稱的時間線,具體做法是,在滾動容器上新增一個屬性scroll-timeline-name,這個屬性值必須以--開頭,就像 CSS 變數一樣,還可以透過scroll-timeline-axis設定滾動方向,此時的animation-timeline就不用預設的scroll()了,而是改用前面設定的變數,示意如下

@keyframes animate-it { … }

/*滾動容器*/
.scroller {
  scroll-timeline-name: --my-scroller;
  scroll-timeline-axis: inline;
}

.scroller .subject {
  animation: animate-it linear;
  animation-timeline: --my-scroller;
}

這裡的scroll-timeline-axisscroll-timeline-name還可以簡寫成一個屬性scroll-timeline

scroll-timeline-name: --my-scroller;
scroll-timeline-axis: inline;
/**可簡寫為**/
scroll-timeline: --my-scroller inline;

下面來看一個橫向滾動的例子,剛好可以把上面的幾個新概念都用上。

佈局還是類似,只是放在了一個可以橫向滾動的容器中

<main>
  <div class="progress"></div>
  ...很多內容...
</main>

main設定橫向滾動,.progress設定fixed定位,還有動畫和上個例子一樣

main{
  display: flex;
  overflow: scroll;
}
.progress{
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 10px;
  background-color: #F44336;
  transform-origin: 0 50%;
  animation:grow-progress 3s linear;
}
@keyframes grow-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

由於這裡main才是滾動容器,並不是頁面,而.progressfixed定位,如果直接用scroll(nearest)獲取到的就是頁面根容器,並不是main,所以這裡需要用命名scroll-timeline,實現如下

main{
  /**/
  scroll-timeline: --scrollcontainer inline;
}
.progress{
  /**/
  animation-timeline: --scrollcontainer;
}

這樣就可以將橫向滾動進度一一對映到動畫上了,而且不受結構限制,非常自由

四、CSS 檢視進度時間線

檢視進度時間線(view progress timeline。這個名字有些難以理解,其實表示的是一個元素出現在頁面視野範圍內的進度,也就是關注的是元素自身位置。元素剛剛出現之前代表 0% 進度,元素完全離開之後代表 100% 進度。

概念非常像JS中的Intersection_Observer_API,也就交叉觀察者,可以監測到元素在可視區的情況,因此,在這種場景中,無需關注滾動容器是哪個,只用處理自身就行了。

和前面的scroll progress time語法類似,也有一個快捷語法

animation-timeline: view()

由於無需關注滾動容器,所以它的引數也不一樣,分別是<axios><inset>

<axios>表示滾動方向,支援以下幾個關鍵值

  • block:滾動容器的塊級軸方向*(預設)*。
  • inline:滾動容器內聯軸方向。
  • y:滾動容器沿 y 軸方向。
  • x:滾動容器沿 x 軸方向。

<inset>表示調整元素的視區範圍,有點類似scroll-padding,支援兩個值,表示開始和結束兩個範圍。

animation-timeline: view(auto); /* 預設值 */
animation-timeline: view(20%);
animation-timeline: view(200px);
animation-timeline: view(20% 40%);
animation-timeline: view(20% 200px);
animation-timeline: view(100px 200px);
animation-timeline: view(auto 200px);

這裡的<inset>還可以用view-timeline-inset單獨來表示,不過需要注意的是,這種用法要使用命名的view progress time,如下

scroll-timeline: --my-scroller block;
view-timeline-inset: 20% 200px;
animation-timeline: --my-scroller;
按照我的經驗,view progress time中使用命名的情況比較少,因為無需知道滾動容器,因此推薦用view()

下面來看一個例子,有一個列表

<div>歡</div>
<div>迎</div>
<div>關</div>
<div>注</div>
<div>前</div>
<div>端</div>
<div>偵</div>
...

簡單修飾後效果如下

CSS 滾動驅動動畫終於正式支援了

現在,我們新增一個淡入和縮放的動畫

@keyframes appear {
  from {
    opacity: 0;
    transform: scaleX(0);
  }
  to {
    opacity: 1;
    transform: scaleX(1);
  }
}

然後透過animation-time繫結在每個元素上,因為我們想做一個元素進入的動畫,所以要用到view progress timeline

div{
  /**/
  animation: appear 1s linear both;
  animation-timeline: view();
}

可以得到這樣的效果

CSS 滾動驅動動畫終於正式支援了

效果是出來了,不過好像有點太過了,太誇張了,可以看到,每個元素在滾動出現到離開的過程中都完整的執行了我們定義的動畫。那麼,有沒有辦法讓這個範圍變小一點呢?預設的範圍如下

CSS 滾動驅動動畫終於正式支援了

當然也是可以的,這裡就需要用到view的第二個引數<inset>了,比如設定40% 0表示調整視區範圍,相當於將滾動容器上邊距減少了 40%,當滾動到視區上面40%的時候就完成了動畫(預設是滾動到0%,也就是完全離開的時候)

div{
  /**/
  animation-timeline: view(40% 0);
}

CSS 滾動驅動動畫終於正式支援了

還可以更加激進一點,設定成100%,相當於元素一旦完全進入,動畫就執行完成了,這樣元素出現動畫會更加和諧

div{
  /**/
  animation-timeline: view(100% 0);
}

此時的動畫範圍就更小了,如下

CSS 滾動驅動動畫終於正式支援了

效果如下,是不是感覺沒那麼誇張了呢

五、CSS 動畫範圍區間

預設情況下,動畫會根據滾動區間範圍一一對映,就比如第一個滾動指示器的例子,滾動多少,指示器的進度就走多少。

CSS 滾動驅動動畫終於正式支援了

但有時候,我們並不需要完整的區間,比如這個例子,右下角的返回頂部按鈕

像這種情況下,我們其實只需要前面滾動一定距離就可以讓返回按鈕完全出現了,對應關係應該是這樣

CSS 滾動驅動動畫終於正式支援了

那麼,如何擷取一定的滾動區間呢?這就要涉及一個新的屬性,叫做animation-range,也就是“動畫範圍”。

這裡也要分兩種場景,也就是前面提到的滾動進度時間線檢視進度時間線

1. 滾動進度時間線

首先來看scroll()場景,由於只是滾動容器的監聽,因此比較簡單,直接設定範圍就行了

animation-range: normal; /* 等價於 normal normal */
animation-range: 20%; /* 等價於 20% normal */
animation-range: 100px; /* 等價於 100px normal */

比如上面這個返回頂部的例子,動畫其實很簡單,就是一個向上的位移動畫

@keyframes back-progress {
  from { transform: translateY(150%); }
  to { transform: translateY(0%); }
}

如果僅僅新增一個滾動時間軸

.back{
  /**/
  animation: back-progress 1s linear forwards;
  animation-timeline: scroll();
}

那麼,這個返回按鈕就像滾動進度條那樣,慢慢的出來,直到滾動到最底部才完全出來

這時只需要在[0, 固定距離]的範圍內出現就好了,表示只在這個區間範圍內觸發動畫,關鍵程式碼如下

.back{
  /**/
  animation: back-progress 1s linear forwards;
  animation-timeline: scroll();
  animation-range: 0 100px;
}

這樣就實現了滾動100px時自動出現的返回頂部按鈕,100px後按鈕會一直顯示

還有一個頭部吸頂的例子,原理也是類似的,如下

頭部是一個高度和字號不斷變小的動畫,然後需要設定一下animation-range,關鍵實現如下

@keyframes header {
  to { 
    height: 60px;
    font-size: 30px;
  }
}
.header{
  /**/
  animation: header 1s linear forwards;
  animation-timeline: scroll();
  animation-range: 0 calc(100vh - 60px);
}

2. 檢視進度時間線

再來看看view()場景。由於涉及到元素和可視區域的交叉,情況稍微複雜一些,如下

animation-range: cover; /* 等價於 cover 0% cover 100% */
animation-range: contain; /* 等價於 contain 0% contain 100% */
animation-range: cover 20%; /* 等價於 cover 20% cover 100% */
animation-range: contain 100px; /* 等價於 contain 100px cover 100% */


animation-range: normal 25%;
animation-range: 25% normal;
animation-range: 25% 50%;
animation-range: entry exit; /* 等價於 entry 0% exit 100% */
animation-range: cover cover 200px; /* 等價於 cover 0% cover 200px */
animation-range: entry 10% exit; /* 等價於 entry 10% exit 100% */
animation-range: 10% exit 90%;
animation-range: entry 10% 90%;

有以下關鍵詞

  • cover:元素首次開始進入滾動容器可見範圍(0%)到完全離開的過程(100% ),也就是元素只需要和可視範圍有交集(預設)
  • contain:元素完全進入滾動容器可見範圍(0%)到剛好要離開的過程(100% ),也就是元素必須完全在可見範圍才會觸發
  • entry:元素進入滾動容器可見範圍的過程,剛進入是 0%,完全進入是 100%
  • exit:元素離開滾動容器可見範圍的過程,剛離開是 0%,完全離開是 100%
  • entry-crossing:和entry比較類似,暫時沒有發現明顯差異
  • exit-crossing:和exit比較類似,暫時沒有發現明顯差異

下面做了一個示意圖,表示各自的範圍區間

CSS 滾動驅動動畫終於正式支援了

如果還是不太清楚,可以用下面這個工具去對比各自的差異

CSS 滾動驅動動畫終於正式支援了

比如前面的列表進入時的動畫,之前是用view(100% 0)實現的,大家有沒有發現,這個效果其實和entry的示意效果一樣的?

CSS 滾動驅動動畫終於正式支援了

如果用animation-range就很好理解了,這裡需要進入動畫,所以可以直接用entry

div{
  animation: appear 1s linear forwords;
  animation-timeline: view();
  animation-range: entry; /*只在進入過程中生效*/
}

同樣可以實現相同的效果。

除此之外還可以同時設定進入和離開兩種動畫,這就需要定義兩個動畫,然後分別給兩個動畫定義動畫區間,關鍵實現如下

div{
  animation: appear 1s linear forwards, 
            disappear 1s linear forwards;
  animation-timeline: view();
  animation-range: entry,exit; /*進入過程執行appear,離開過程執行disappear*/
}

/*出現動畫*/
@keyframes appear {
  0% {
    opacity: 0;
    transform: scaleX(0);
  }

  100% {
    opacity: 1;
    transform: scaleX(1);
  }
}
/*離開*/
@keyframes disappear {
  100% {
    opacity: 0;
    transform: scaleX(0);
  }

  0% {
    opacity: 1;
    transform: scaleX(1);
  }
}

這樣就得到一個進入和離開均存在動畫的滾動列表

另外,還可以將animation-range合併到同一個動畫中,在關鍵幀前面加上entry這些關鍵詞,這樣就無需指定animation-range中了

六、用一張圖總結一下

總的來說,CSS 滾動驅動動畫為以後的互動帶來了無限可能,下面用一張圖總結一下

CSS 滾動驅動動畫終於正式支援了

相關文章