HTML&CSS
CSSだけでスクロールアニメーション!Scroll-driven Animations、animation-timelineプロパティを解説
ウェブページで、スクロールしたら変化するアニメーションは、現時点ではJavaScriptで実装するのが主流です。
それをCSSで実現するための仕様策定が進んでおり、Chrome115以降およびChrome115以降が中身に使われているedgeでは対応がされています。
その仕様の名称が「Scroll-driven Animations」です。
この記事では、実際に「Scroll-driven Animations」を実装したデモをお見せしながら、どのように使えるものなのかご紹介します。早くSafariでも使えるようになるといいですね!
Scroll-driven Animationsとは?
「Scroll-driven Animations」はCSSの新しい仕様です。スクロールに応じたアニメーションをCSSで実装できるようにW3Cが仕様策定を進めていて、2023年12月現在で草案の状態ですが、2023年7月にアップデートされたChrome115以降、およびChrome115以降が中身に使われているedgeではサポートがされています。
なお、現時点では、スクロールに応じたアニメーションはJavaScriptを使って実装するのが主流で、今すぐ実案件で活用するならばJavaScriptでの対応になると思います。当サイトでも次の記事で実装方法についてご紹介しています。
スクロールしたら動くアニメーションを素のJavaScriptとCSSで実装
スクロールアニメーションのメリット
スクロールしたら動くアニメーションは視覚的に豊かさを与え、うまく使えば心地よさをユーザーに与えることができます。
また、着目して欲しいところを強調する役割も果たすことができます。
CSS(Scroll-driven Animations)のみの実装とJavaScriptを使った実装方法の違い
JavaScriptを使った実装の理屈
JavaScriptを使った実装では、対象とする要素が画面内に入ったかどうかをJavaScriptで検知し、検知されたらその要素に特定のクラス名を付与するという方法が一般的です。
そのクラス名に対して、CSSでアニメーションを与えておくことによって、画面内に入ったら初めてそのアニメーションが動くようになる、というのが理屈です。
CSS(Scroll-driven Animations)のみを使った実装方法の理屈
Scroll-driven Animationsは、対象要素にCSSのanimation-timeline
プロパティで関数scroll()
またはview()
を与え、同時にCSSでアニメーションの動きを与えるだけで、その要素がスクロールに連動して動くようになります。
.demo{
animation: expand ease;
animation-timeline: scroll(); //これが新しい仕様
}
Scroll-driven Animationsのデモ
それでは、実際にいくつかのパターンを見てみましょう。Macやiphoneで見る場合、Safariだと動かないので、Chromeでお試しください。
拡大する
See the Pen 大きくなる by Takahiro Inada (@tkhr1) on CodePen.
色が変化する
See the Pen 色が変わる by Takahiro Inada (@tkhr1) on CodePen.
徐々に伸びる
See the Pen 徐々に埋まる by Takahiro Inada (@tkhr1) on CodePen.
横に動く
See the Pen 横に動く by Takahiro Inada (@tkhr1) on CodePen.
回転する
See the Pen Untitled by Takahiro Inada (@tkhr1) on CodePen.
形が変わる
See the Pen 形が変わる by Takahiro Inada (@tkhr1) on CodePen.
透明度が変わる
See the Pen Untitled by Takahiro Inada (@tkhr1) on CodePen.
ブラー(ぼかし)に変化をつける
See the Pen Untitled by Takahiro Inada (@tkhr1) on CodePen.
パララックス
See the Pen パララックス by Takahiro Inada (@tkhr1) on CodePen.
ここまでの技を組み合わせて、ちょっとした作品
Thank you.
HTML
<div class="demo-frame">
<div class="circle"></div>
<div class="inner-01"></div>
<div class="inner-02">
<div class="box parallax-01 color-01"></div>
<div class="box parallax-01 color-02"></div>
<div class="box parallax-01 color-03"></div>
<div class="box parallax-01 color-01"></div>
<div class="box parallax-01 color-02"></div>
<div class="box parallax-02 color-03"></div>
<div class="box parallax-02 color-01"></div>
<div class="box parallax-02 color-02"></div>
<div class="box parallax-02 color-03"></div>
<div class="box parallax-02 color-01"></div>
<div class="box parallax-02 color-02"></div>
<div class="box parallax-03 color-03"></div>
<div class="box parallax-03 color-01"></div>
<div class="box parallax-03 color-02"></div>
<div class="box parallax-03 color-03"></div>
<p class="ending">Thank you.</p>
</div>
</div>
CSS
<style>
.demo-frame{
width: 100%;
height: 500px;
overflow-y: scroll;
position: relative;
background-color: #333;
scrollbar-width: thin;
scrollbar-color: #f1f1f1 transparent;
}
.demo-frame::-webkit-scrollbar{
width: 10px;
}
.demo-frame::-webkit-scrollbar-track{
background-color: transparent;
border-radius: 5px;
}
.demo-frame::-webkit-scrollbar-thumb{
background-color: #ccc;
border-radius: 5px;
}
.inner-01{
height: 100vh;
width: 100%;
}
.inner-02{
position: relative;
height: 500vh;
width: 100%;
}
.circle{
position: sticky;
top: 250px;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #ccc;
animation: side-scroll ease forwards;
animation-timeline: scroll();
animation-range: contain 0% contain 20%;
left: 0;
}
@keyframes side-scroll{
0%{left:0; backgroud-color:#ccc;}
50%{left:50%; opacity: 1; transform: scale(1); background-color: #fff;}
100%{left: 50%; opacity: 0; transform: scale(100);}
}
.box{
position: absolute;
width: 5px;
height: 30px;
background-color: #fff;
}
.parallax-01.color-01{
animation: parallax-01 linear, color-01 ease forwards;
animation-timeline: scroll();
}
.parallax-01.color-02{
animation: parallax-01 linear, color-02 ease forwards;
animation-timeline: scroll();
}
.parallax-01.color-03{
animation: parallax-01 linear, color-03 ease forwards;
animation-timeline: scroll();
}
.parallax-02.color-01{
animation: parallax-02 linear, color-01 ease forwards;
animation-timeline: scroll();
}
.parallax-02.color-02{
animation: parallax-02 linear, color-02 ease forwards;
animation-timeline: scroll();
}
.parallax-02.color-03{
animation: parallax-02 linear, color-03 ease forwards;
animation-timeline: scroll();
}
.parallax-03.color-01{
animation: parallax-03 linear, color-01 ease forwards;
animation-timeline: scroll();
}
.parallax-03.color-02{
animation: parallax-03 linear, color-02 ease forwards;
animation-timeline: scroll();
}
.parallax-03.color-03{
animation: parallax-03 linear, color-03 ease forwards;
animation-timeline: scroll();
}
@keyframes parallax-01{
0%{transform: translateY(1000px);}
100%{transform: translateY(0);}
}
@keyframes parallax-02{
0%{transform: translateY(0);}
100%{transform: translateY(3000px);}
}
@keyframes parallax-03{
0%{transform: translateY(0)}
100%{transform: translateY(2000px);}
}
@keyframes color-01{
0%{background-color: #ffa742;}
100%{background-color: rgb(52, 231, 255);}
}
@keyframes color-02{
0%{background-color: #39e4db;}
100%{background-color: #e439a2;}
}
@keyframes color-03{
0%{background-color: #ae39e4;}
100%{background-color: #ffe711;}
}
.box:nth-of-type(1){top: 100px; left:calc(50% - 100px); }
.box:nth-of-type(2){top: 200px; left:calc(50% - 200px); }
.box:nth-of-type(3){top: 300px; left:calc(50% - 60px); }
.box:nth-of-type(4){top: 500px; left:calc(50% + 250px); }
.box:nth-of-type(5){top: 100px; left:calc(50% + 30px); }
.box:nth-of-type(6){top: 200px; left:calc(50% - 300px); }
.box:nth-of-type(7){top: 300px; left:calc(50% + 300px); }
.box:nth-of-type(8){top: 500px; left:calc(50% - 250px); }
.box:nth-of-type(9){top: 100px; left:calc(50% + 350px); }
.box:nth-of-type(10){top: 100px; left:calc(50% + 200px); }
.box:nth-of-type(11){top: 100px; left:calc(50% - 100px); }
.box:nth-of-type(12){top: 200px; left:calc(50% - 300px); }
.box:nth-of-type(13){top: 300px; left:calc(50% - 500px); }
.box:nth-of-type(14){top: 500px; left:calc(50% + 100px); }
.box:nth-of-type(15){top: 100px; left:calc(50% + 400px); }
.ending{
color: #fff!important;
position: absolute;
bottom: 500px;
animation: ending;
animation-timeline: view();
animation-range: cover 0% cover 100%;
text-align: center;
width: 100%;
font-size: 30px;
}
@keyframes ending{
0%{opacity:1;}
50%{opacity:1; filter:blur(0);}
100%{opacity:0; filter:blur(8px);}
}
</style>
Scroll-driven Animationsの解説
Scroll-driven Animationsは、次の2種類があります。
animation-timeline: scroll(); /* Scroll Progress Timeline */
animation-timeline: view(); /* View Progress Timeline */
それぞれがどう違うか解説致します。
Scroll Progress Timeline
概要
「Scroll Progress Timeline」は、
animation: demo;
animation-timeline: scroll();
のように指定することで、ユーザーがページをどれだけスクロールしたかに応じてアニメーションが進行します。animation-timeline
プロパティはanimation
プロパティよりも後で記述しなければいけません。
また、animation
プロパティのanimation-duration
に秒を指定しても意味はありません。
スクロール量に直接関係し、ページの特定の位置にアニメーションを固定することができます。
scoll()
scroll()
は第一引数に <scroller>
を、第二引数に <axis>
を受け取ることができます。
scroll( <scroller> <axis> );
<scroller>
の値は次の通りです。何も指定しない場合のデフォルト値はnearst
です。
nearest
- もっとも近い祖先のスクロールコンテナを使用します(デフォルト)。
root
- ドキュメントビューポートをスクロールコンテナとして使用します。
self
- その要素自体をスクロールコンテナとして使用します。
<axis>
の値は次のとおりです。何も指定しない場合のデフォルト値はblock
です。
block
- スクロールコンテナのブロック軸に沿った進行状況の指標を使用します(デフォルト)。
inline
- スクロールコンテナのインライン軸に沿った進行状況の指標を使用します。
y
- スクロールコンテナのy軸に沿った進行状況の指標を使用します。
x
- スクロールコンテナのx軸に沿った進行状況の指標を使用します。
View Progress Timeline
概要
「View Progress Timeline」は、
animation: demo;
animation-timeline: view();
animation-range: contain 0% contain 100%;
のように指定することで、特定の要素がスクロールポート内にどれだけ表示されているかに応じてアニメーションが進行します。animation-timeline
プロパティはanimation
プロパティよりも後で記述しなければいけません。
例えば、スクロールポートに特定の要素が完全に表示されたときにアニメーションを開始し、スクロールポートから消えると完了するような場合に適しています。
animation-rangeプロパティ
「View Progress Timeline」の場合、
animation-range: contain 0% cover 100%;
のように、アニメーションの開始位置と終了位置を指定できます。
指定できるのは次の通りです。
cover
- 対象要素がスクロールポート内に少しでも入った時に変化を開始し、完全に出るタイミングで変化が終わります。
contain
- 対象要素がスクロールポート内に完全に入った時に変化を開始し、少しでも出た時に変化が終わります。
entry
- 対象要素がスクロールポート内に少しでも入った時に変化を開始し、完全に入ったタイミングで変化が終わります。
exit
- 対象要素がスクロールポート内から少しでも出たタイミングで変化を開始し、完全に出たタイミングで変化が終わります。
entry-crossing
- entry-crossingはentryと似ており、対象要素がスクロールポート内になかったとしても、スクロールポート終了地点の境界線を横切ったらアニメーションが開始するという点が異なります。
exit-crossing
- exit-crossingはexitと似ており、対象要素がスクロールポート内になかったとしても、スクロールポート開始地点の境界線を横切ったらアニメーションが開始するという点です。
View Progress Timelineのシミュレーション
「View Progress Timeline」はややこしいので、動かしながら仕様を理解できるように、シミュレーションを作りました。よろしければchrome115以降またはchrome115以降が使われているedgeでご確認ください。
シミュレーション
50px | ||
|
||
0% | ||
|
||
100% |
animation-range: contain 0% contain 100%;
↓スクロールしてみてください。
変形する要素
スクロールバーをつかみながらゆっくりスクロールすることで、どの地点で変化が開始し、どの地点で変化が終了するかが把握していただけると思います。
なお、まだ新しい仕様なので、chrome115以降であったとしても、環境により本来の仕様とは異なる挙動を示すケースは起こりやすいと思います。あれ?おかしいな、と思う部分はその前提で見た方が良いかもしれません。
JavaScriptによる実装と比べてのメリットデメリットと使い分けについて
Scroll-driven Animationsはご覧の通り素晴らしい仕様ですが、現在の仕様のままだとすると、スクロールに連動したアニメーションの実装はJavaScriptによる実装と、Scroll-driven Animationsの使い分けになっていくのではないかと考えられます。
以下に、それぞれの違い、メリットについてご紹介します。
Scroll-driven Animationsが優れている点
両方向のスクロールに連動する
Scroll-driven Animationsは一方向へのスクロール時だけでなく、両方向のスクロールに連動して動きます。JavaScriptでの実装でも両方向に連動させられなくはないのですが、それをやると動作がかなり重くなるので、一方向の連動だけにしているサイトがほとんどだと思います。
1px単位で連動するリッチな表現を軽い動作で実装できる
JavaScriptでの実装は、クラス名の付与により実現する手法が一般的で、1px単位でスクロールに連動させようとすると動作が重くなるので、通常そのような実装は行いません。Scroll-driven Animationsは1px単位でスクロールに連動するので、Scroll-driven Animationsでなければ難しいリッチな表現が可能となります。
Scroll-driven Animationsでは出来ないこと
フワッと動く表現ができない
JavaScriptの場合、クラス名が付いた瞬間にアニメーションが動くので、画面内にその要素が入ったタイミングでフワッと動かすことができますが、Scroll-driven Animationsはあくまでもスクロールに連動しての変化となるので、スクロールを止めれば動きが止まり、フワッとした動きは実現できません。
その他のスクロールを止めた時に表現ができない
スクロールしてアニメーションが動き出したら、しばらく手を止めて見ていたくなるようなアニメーションもあると思いますが、Scroll-driven Animationsはスクロールと連動して動くものなので、そのようなアニメーションは実装できないということになります。
ブラウザのサポート状況
ブラウザの最新のサポート状況はこちらで確認できます。
現時点ではSafariやFirefoxがサポートしていません。まだ仕様が勧告ではなく草案(draft)の状況なので、仕方がないですけどね。