スクロール タイムラインとビュー タイムラインを使用して、宣言型の方法でスクロールドリブン アニメーションを作成する方法について説明します。
スクロールドリブン アニメーション
スクロールドリブン アニメーションは、ウェブ上で一般的な UX パターンです。スクロールドリブン アニメーションは、スクロール コンテナのスクロール位置にリンクされます。つまり、上下にスクロールすると、リンクされたアニメーションがダイレクト レスポンスで前後にスクラブされます。たとえば、視差効果のある背景画像や、スクロールすると移動する読み上げインジケーターなどの効果があります。
同様のタイプのスクロールドリブン アニメーションも、スクロール コンテナ内の要素の位置にリンクされているアニメーションです。たとえば、要素を表示したときにフェードインできます。
この種の効果を実現するには、スクロール イベントにメインスレッドで応答するというのが従来の方法です。この場合、次の 2 つの大きな問題が発生します。
- 最新のブラウザはスクロールを別のプロセスで実行するため、スクロール イベントを非同期で配信します。
- メインスレッドのアニメーションはジャンクの影響を受けます。
このため、スクロールと同期したパフォーマンスの高いスクロールドリブン アニメーションを作成することは不可能であるか、非常に困難になります。
Chrome バージョン 115 以降では、宣言型スクロールドリブン アニメーションを実現するために使用できる新しい API とコンセプトのセットとして、スクロール タイムラインとビュー タイムラインが導入されています。
これらの新しいコンセプトは、既存の Web Animations API(WAAPI)や CSS Animations API と統合されるため、これらの既存の API の利点を継承できます。これには、メインスレッド以外でスクロールドリブン アニメーションを実行する機能が含まれます。ぜひご一読ください。数行のコードを追加するだけで、スクロールによって滑らかなアニメーションを実現し、メインスレッドから実行できるようになりました。嫌いなものは?!
ウェブ用アニメーション(まとめ)
CSS を使用してウェブ上でアニメーションを作成する
CSS でアニメーションを作成するには、@keyframes
@ ルールを使用して一連のキーフレームを定義します。animation-name
プロパティを使用して要素にリンクし、animation-duration
を設定してアニメーションの所要時間を決定します。他にも、animation-*
の省略形プロパティ(animation-easing-function
や animation-fill-mode
)が他にもあります。これらは、すべて animation
の短縮形にまとめることができます。
たとえば、以下のアニメーションでは、背景色も変更しながら X 軸上の要素をスケールアップします。
@keyframes scale-up {
from {
background-color: red;
transform: scaleX(0);
}
to {
background-color: darkred;
transform: scaleX(1);
}
}
#progressbar {
animation: 2.5s linear forwards scale-up;
}
JavaScript を使用したウェブでのアニメーション
JavaScript でも、Web Animations API を使用してまったく同じことを実現できます。そのためには、新しい Animation
インスタンスと KeyFrameEffect
インスタンスを作成するか、より短い Element
animate()
メソッドを使用します。
document.querySelector('#progressbar').animate(
{
backgroundColor: ['red', 'darkred'],
transform: ['scaleX(0)', 'scaleX(1)'],
},
{
duration: 2500,
fill: 'forwards',
easing: 'linear',
}
);
上記の JavaScript スニペットの表示結果は、前のバージョンの CSS と同一です。
アニメーション タイムライン
デフォルトでは、要素に適用されているアニメーションはドキュメント タイムラインで実行されます。ページが読み込まれると、オリジンの時刻は 0 から始まり、時間の経過とともに進むようになります。これはアニメーション タイムラインのデフォルトですが、これまでは、この他に利用できるアニメーション タイムラインはありませんでした。
スクロールドリブン アニメーションの仕様では、使用できる次の 2 種類のタイムラインが定義されています。
- スクロール進行状況タイムライン: 特定の軸に沿ったスクロール コンテナのスクロール位置にリンクされているタイムライン。
- ビュー進行状況タイムライン: スクロール コンテナ内の特定の要素の相対位置にリンクされているタイムライン。
スクロール進行状況タイムライン
スクロール進行状況タイムラインは、特定の軸に沿ったスクロール コンテナのスクロール位置の進行状況にリンクされているアニメーション タイムラインです。スクロール ポートまたはスクローラーとも呼ばれます。スクロール範囲内の位置を進行状況の割合に変換します。
スクロールの開始位置の進行状況は 0%、終了位置は 100% です。以下の可視化では、スクローラーを上から下にスクロールすると、進行状況が 0% から 100% まで増えることがわかります。
✨ 試してみる
スクロール進行状況タイムラインは、単に「スクロール タイムライン」と省略されることがよくあります。
ビュー進行状況タイムライン
このタイプのタイムラインは、スクロール コンテナ内の特定の要素の相対的な進行状況にリンクしています。スクロール進行状況タイムラインと同様に、スクローラーのスクロール オフセットが追跡されます。スクロール進行状況タイムラインとは異なり、スクローラー内の対象の相対的な位置によって進行状況が決まります。
これは、スクローラーで要素がどの程度表示されているかを追跡できる IntersectionObserver
の仕組みとある程度同等です。要素がスクローラー内に表示されていなければ、要素は交差していません。わずかでもスクローラー内に表示されていれば、要素は交差しています。
ビュー進行状況タイムラインは、対象がスクローラーとの交差を開始した瞬間から開始し、スクローラーとの交差を停止すると終了します。以下の可視化では、対象がスクロール コンテナに入った時点で進行状況が 0% から開始し、対象がスクロール コンテナから離れた時点で 100% になることがわかります。
✨ 試してみる
多くの場合、ビュー進行状況タイムラインは単に「ビュー タイムライン」と省略されます。ビュー タイムラインの特定の部分を被験者のサイズに基づいてターゲットにすることは可能ですが、これについては後で詳しく説明します。
スクロール進行状況タイムラインの実践的活用
CSS で匿名のスクロール進行状況タイムラインを作成する
CSS でスクロール タイムラインを作成する最も簡単な方法は、scroll()
関数を使用することです。これにより、匿名のスクロール タイムラインが作成され、新しい animation-timeline
プロパティの値として設定できます。
例:
@keyframes animate-it { … }
.subject {
animation: animate-it linear;
animation-timeline: scroll(root block);
}
scroll()
関数は、<scroller>
引数と <axis>
引数を受け入れます。
<scroller>
引数で使用できる値は次のとおりです。
nearest
: 最も近い祖先スクロール コンテナを使用します(デフォルト)。root
: ドキュメントのビューポートをスクロール コンテナとして使用します。self
: 要素自体をスクロール コンテナとして使用します。
<axis>
引数で使用できる値は次のとおりです。
block
: スクロール コンテナのブロック軸で進行状況を測定します。inline
: スクロール コンテナのインライン軸で進行状況を測定します。y
: スクロール コンテナの Y 軸で進行状況を測定します。x
: スクロール コンテナの X 軸で進行状況を測定します。
たとえば、アニメーションをブロック軸のルート スクローラーにバインドする場合、scroll()
に渡す値は root
と block
です。合わせて scroll(root block)
となります。
デモ: 読み上げの進行状況インジケーター
このデモでは、読書の進行状況インジケーターがビューポートの上部に固定されています。ページを下にスクロールすると、進行状況バーがドキュメントの最後に達してビューポートの幅いっぱいまで拡大します。アニメーションの駆動には、匿名のスクロール進行状況タイムラインが使用されます。
✨ 試してみる
読書の進行状況インジケーターは、固定の位置を使用してページの上部に配置されます。合成アニメーションを活用するには、width
ではなく、transform
を使用して x 軸上で要素が縮小されます。
<body>
<div id="progress"></div>
…
</body>
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
#progress {
position: fixed;
left: 0; top: 0;
width: 100%; height: 1em;
background: red;
transform-origin: 0 50%;
animation: grow-progress auto linear;
animation-timeline: scroll();
}
#progress
要素のアニメーション grow-progress
のタイムラインは、scroll()
を使用して作成された匿名タイムラインに設定されています。scroll()
は引数が指定されていないため、デフォルト値にフォールバックします。
追跡するデフォルトのスクローラーは nearest
であり、デフォルトの軸は block
です。これにより、ルート スクローラーが #progress
要素の最も近いスクローラーであるため、ブロックの方向を追跡しながら、効果的にターゲットになります。
CSS で名前付きスクロール進行状況タイムラインを作成する
また、名前付きのスクロール進行状況タイムラインを定義する方法もあります。これは少し冗長ですが、親スクローラーやルート スクローラーをターゲットにしていない場合、ページで複数のタイムラインを使用している場合、または自動検索が機能しない場合に便利です。この方法では、任意の名前でスクロール進行状況タイムラインを識別できます。
要素に名前付きスクロール進行状況タイムラインを作成するには、スクロール コンテナの scroll-timeline-name
CSS プロパティに任意の識別子を設定します。値は --
で始まる必要があります。
トラッキングする軸を微調整するには、scroll-timeline-axis
プロパティも宣言します。使用可能な値は、scroll()
の <axis>
引数と同じです。
最後に、アニメーションをスクロール進行状況タイムラインにリンクします。アニメーションを再生する要素の animation-timeline
プロパティに、scroll-timeline-name
で使用した ID と同じ値を設定します。
コード例:
@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
の短縮形で scroll-timeline-name
と scroll-timeline-axis
を組み合わせることができます。次に例を示します。
scroll-timeline: --my-scroller inline;
デモ: 水平カルーセルのステップ インジケーター
このデモでは、各画像カルーセルの上にステップ インジケーターが表示されます。カルーセルに 3 つの画像が含まれている場合、インジケーター バーの幅は 33% から始まります。これは、現在 3 枚のうちの 1 枚の画像が表示されていることを示します。最後の画像がビュー内にある場合(スクローラーが最後までスクロールしたことで判断)、インジケーターはスクローラーの全幅を占めます。名前付きのスクロール進行状況タイムラインがアニメーションの駆動に使用されます。
✨ 試してみる
ギャラリーの基本マークアップは次のとおりです。
<div class="gallery" style="--num-images: 2;">
<div class="gallery__scrollcontainer">
<div class="gallery__progress"></div>
<div class="gallery__entry">…</div>
<div class="gallery__entry">…</div>
</div>
</div>
.gallery__progress
要素は、.gallery
ラッパー要素内に絶対に配置されます。初期サイズは --num-images
カスタム プロパティによって決まります。
.gallery {
position: relative;
}
.gallery__progress {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1em;
transform: scaleX(calc(1 / var(--num-images)));
}
.gallery__scrollcontainer
は、含まれる .gallery__entry
要素を水平方向にレイアウトする要素であり、スクロールする要素です。スクロール位置をトラッキングすることで、.gallery__progress
がアニメーション化されます。これは、指定されたスクロール進行状況タイムライン --gallery__scrollcontainer
を参照することで行われます。
@keyframes grow-progress {
to { transform: scaleX(1); }
}
.gallery__scrollcontainer {
overflow-x: scroll;
scroll-timeline: --gallery__scrollcontainer inline;
}
.gallery__progress {
animation: auto grow-progress linear forwards;
animation-timeline: --gallery__scrollcontainer;
}
JavaScript を使用してスクロール進行状況タイムラインを作成する
JavaScript でスクロール タイムラインを作成するには、ScrollTimeline
クラスの新しいインスタンスを作成します。追跡する source
と axis
を含むプロパティ バッグを渡します。
source
: スクローラーを追跡する要素への参照。document.documentElement
を使用して、ルート スクローラーをターゲットにします。axis
: トラッキングする軸を指定します。CSS バリアントと同様に、指定できる値はblock
、inline
、x
、y
です。
const tl = new ScrollTimeline({
source: document.documentElement,
});
ウェブ アニメーションにアタッチするには、これを timeline
プロパティとして渡し、duration
がある場合は省略します。
$el.animate({
opacity: [0, 1],
}, {
timeline: tl,
});
デモ: 読書の進行状況インジケーター、再確認
同じマークアップを使用しながら、読書の進行状況インジケーターを JavaScript で再作成するには、次の JavaScript コードを使用します。
const $progressbar = document.querySelector('#progress');
$progressbar.style.transformOrigin = '0% 50%';
$progressbar.animate(
{
transform: ['scaleX(0)', 'scaleX(1)'],
},
{
fill: 'forwards',
timeline: new ScrollTimeline({
source: document.documentElement,
}),
}
);
見た目は CSS バージョンと同じです。作成された timeline
はルート スクローラーを追跡し、ページをスクロールすると、x 軸上の #progress
を 0% から 100% まで拡大します。
✨ 試してみる
ビュー進行状況タイムラインの実践的な使い方
CSS で匿名ビュー進行状況タイムラインを作成する
ビュー進行状況タイムラインを作成するには、view()
関数を使用します。使用できる引数は <axis>
と <view-timeline-inset>
です。
<axis>
はスクロール進行状況タイムラインと同じで、追跡する軸を定義します。デフォルト値はblock
です。<view-timeline-inset>
では、オフセット(正または負)を指定して、要素がビュー内にある(またはない)とみなされる境界を調整できます。値は割合またはauto
にする必要があります。デフォルト値はauto
です。
たとえば、ブロック軸上でスクローラーと交差する要素にアニメーションをバインドするには、view(block)
を使用します。scroll()
と同様に、これを animation-timeline
プロパティの値として設定します。忘れずに animation-duration
を auto
に設定してください。
次のコードを使用すると、すべての img
が、スクロール中にビューポートを横切るときにフェードインします。
@keyframes reveal {
from { opacity: 0; }
to { opacity: 1; }
}
img {
animation: reveal linear;
animation-timeline: view();
}
Intermezzo: タイムライン範囲の表示
デフォルトでは、ビュー タイムラインにリンクされているアニメーションはタイムライン範囲全体に適用されます。これは、対象がスクロールポートに入ろうとした瞬間から始まり、スクロールポートから完全に離れた時点で終了します。
ビュー タイムラインの特定の部分にリンクすることもできます。その場合は、接続する範囲を指定します。たとえば、対象がスクローラーに入った瞬間にリンクできます。以下の例では、対象がスクロール コンテナに入り始めたら進行状況が 0% で開始し、完全に交差した瞬間に 100% になります。
ターゲットに設定できるビュー タイムラインの範囲は次のとおりです。
cover
: ビュー進行状況タイムラインの全範囲を表します。entry
: プリンシパル ボックスがビュー進行状況の表示範囲に入っている途中の状態の範囲を表します。exit
: プリンシパル ボックスがビュー進行状況の表示範囲から出ている途中の状態の範囲を表します。entry-crossing
: プリンシパル ボックスが終了境界エッジと交差している状態の範囲を表します。exit-crossing
: プリンシパル ボックスが開始境界エッジと交差している状態の範囲を表します。contain
: プリンシパル ボックスがスクロール ポート内のビュー進行状況の表示範囲に完全に含まれているか、完全にカバーしている状態の範囲を表します。これは、対象がスクローラーよりも長いか短いかによって変わります。
範囲を定義するには、範囲の開始と終了を設定する必要があります。各値は、範囲名(上記のリストを参照)と、その範囲名内での位置を判別するための範囲オフセットで構成されます。範囲オフセットは通常、0%
~100%
のパーセンテージですが、20em
などの固定長を指定することもできます。
たとえば、対象が入った瞬間からアニメーションを実行する場合は、範囲の開始点として entry 0%
を選択します。サブジェクトの入力時間までに終了するには、範囲終了の値として entry 100%
を選択します。
CSS では、animation-range
プロパティを使用してこれを設定します。例:
animation-range: entry 0% entry 100%;
JavaScript では、rangeStart
プロパティと rangeEnd
プロパティを使用します。
$el.animate(
keyframes,
{
timeline: tl,
rangeStart: 'entry 0%',
rangeEnd: 'entry 100%',
}
);
以下の埋め込みツールを使用して、各範囲名が何を表しているか、および割合が開始位置と終了位置にどのように影響するかを確認してください。範囲の開始を entry 0%
、終了を cover 50%
に設定し、スクロールバーをドラッグしてアニメーションの結果を確認します。
録画を見る
[View Timeline Ranges] ツールでいろいろと試していると、一部の範囲は 2 つの異なる範囲名と範囲オフセットの組み合わせでターゲットにできます。たとえば、entry 0%
、entry-crossing 0%
、cover 0%
はすべて同じエリアをターゲットにします。
「範囲の開始」と「範囲の終了」が同じ範囲名を対象とし、範囲全体(0% から 100% まで)の場合は、範囲名だけに値を短くできます。たとえば、animation-range: entry 0% entry 100%;
をより短い animation-range: entry
に書き換えることができます。
デモ: 画像表示
このデモでは、画像がスクロールポートに入るとフェードインします。これは匿名ビュー タイムラインを使用して行われます。アニメーションの範囲が微調整され、各画像がスクローラーの中間にあるときに完全に不透明になるようにしました。
✨ 試してみる
拡大効果は、アニメーション化されたクリップパスを使用することで実現できます。このエフェクトに使用する CSS は次のとおりです。
@keyframes reveal {
from { opacity: 0; clip-path: inset(0% 60% 0% 50%); }
to { opacity: 1; clip-path: inset(0% 0% 0% 0%); }
}
.revealing-image {
animation: auto linear reveal both;
animation-timeline: view();
animation-range: entry 25% cover 50%;
}
CSS で名前付きビュー進行状況タイムラインを作成する
スクロール タイムラインに名前付きバージョンがあるのと同様に、名前付きビュー タイムラインを作成することもできます。scroll-timeline-*
プロパティの代わりに、view-timeline-
接頭辞を持つバリアント(view-timeline-name
と view-timeline-axis
)を使用します。
同じタイプの値が適用され、名前付きタイムラインを検索する場合と同じルールが適用されます。
デモ: 画像の表示、再確認
先ほどの画像公開デモを手直しすると、コードは次のようになります。
.revealing-image {
view-timeline-name: --revealing-image;
view-timeline-axis: block;
animation: auto linear reveal both;
animation-timeline: --revealing-image;
animation-range: entry 25% cover 50%;
}
view-timeline-name: revealing-image
を使用すると、要素は最も近いスクローラー内でトラッキングされます。同じ値が animation-timeline
プロパティの値として使用されます。視覚的な出力は以前とまったく同じです。
✨ 試してみる
JavaScript でビュー進行状況タイムラインを作成する
JavaScript でビュー タイムラインを作成するには、ViewTimeline
クラスの新しいインスタンスを作成します。追跡する subject
、axis
、inset
を含むプロパティ バッグを渡します。
subject
: 独自のスクローラー内でトラッキングする要素への参照。axis
: 追跡する軸。CSS バリアントと同様に、指定できる値はblock
、inline
、x
、y
です。inset
: ボックスがビュー内にあるかどうかを判断する際のスクロール ポートのインセット(正)またはアウトセット(負)の調整。
const tl = new ViewTimeline({
subject: document.getElementById('subject'),
});
ウェブ アニメーションにアタッチするには、これを timeline
プロパティとして渡し、duration
がある場合は省略します。必要に応じて、rangeStart
プロパティと rangeEnd
プロパティを使用して範囲情報を渡します。
$el.animate({
opacity: [0, 1],
}, {
timeline: tl,
rangeStart: 'entry 25%',
rangeEnd: 'cover 50%',
});
✨ 試してみる
その他のおすすめ
1 つのキーフレーム セットを使用して複数のビュー タイムライン範囲に接続する
連絡先リストがアニメーション表示されるデモを見てみましょう。リストエントリが下からスクロールポートに入るとスライド + フェードインし、上部のスクロールポートから出るとスライド + フェードアウトします。
✨ 試してみる
このデモでは、各要素が 1 つのビュー タイムラインで装飾され、要素がスクロールポートを横切るのをトラッキングします。また、2 つのスクロールドリブン アニメーションがアタッチされています。animate-in
アニメーションはタイムラインの entry
範囲に接続され、animate-out
アニメーションはタイムラインの exit
範囲に接続されます。
@keyframes animate-in {
0% { opacity: 0; transform: translateY(100%); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes animate-out {
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-100%); }
}
#list-view li {
animation: animate-in linear forwards,
animate-out linear forwards;
animation-timeline: view();
animation-range: entry, exit;
}
2 つの異なる範囲に適用された 2 つの異なるアニメーションを実行する代わりに、範囲情報がすでに含まれている 1 つのキーフレーム セットを作成することもできます。
@keyframes animate-in-and-out {
entry 0% {
opacity: 0; transform: translateY(100%);
}
entry 100% {
opacity: 1; transform: translateY(0);
}
exit 0% {
opacity: 1; transform: translateY(0);
}
exit 100% {
opacity: 0; transform: translateY(-100%);
}
}
#list-view li {
animation: linear animate-in-and-out;
animation-timeline: view();
}
キーフレームには範囲情報が含まれているため、animation-range
を指定する必要はありません。結果は以前とまったく同じです。
✨ 試してみる
祖先以外のスクロール タイムラインへのアタッチ
名前付きスクロール タイムラインと名前付きビュー タイムラインの検索メカニズムは、スクロールの祖先のみに制限されています。しかし多くの場合、アニメーション化する必要のある要素は、追跡すべきスクローラーの子ではありません。
これを実現するには、timeline-scope
プロパティを使用します。このプロパティを使用すると、実際に作成せずに、その名前のタイムラインを宣言できます。これにより、その名前のタイムラインの範囲が広がります。実際には、共有親要素で timeline-scope
プロパティを使用して、子スクローラーのタイムラインを接続できるようにします。
次に例を示します。
.parent {
timeline-scope: --tl;
}
.parent .scroller {
scroll-timeline: --tl;
}
.parent .scroller ~ .subject {
animation: animate linear;
animation-timeline: --tl;
}
このスニペットでは:
.parent
要素は、--tl
という名前のタイムラインを宣言します。そのすべての子が検出し、animation-timeline
プロパティの値として使用できます。.scroller
要素は、実際には--tl
という名前のスクロール タイムラインを定義します。デフォルトでは子にのみ表示されますが、.parent
がscroll-timeline-root
として設定しているので、子に接続されます。.subject
要素は--tl
タイムラインを使用します。祖先ツリーを順にたどって、.parent
で--tl
を見つけます。.parent
の--tl
が.scroller
の--tl
を指すため、.subject
は基本的に.scroller
のスクロール進行状況タイムラインを追跡します。
言い換えると、timeline-root
を使用してタイムラインを祖先に移動(ホイスティング)して、祖先のすべての子がタイムラインにアクセスできるようにします。
timeline-scope
プロパティは、スクロール タイムラインとビュー タイムラインの両方で使用できます。
その他のデモとリソース
スクロールドリブン アニメーション.style ミニサイトの記事で説明されているすべてのデモ。このウェブサイトには、スクロールドリブン アニメーションで何が可能であるかを示すデモが多数用意されています。
その他のデモの 1 つとして、以下のアルバムカバーの一覧があります。それぞれのカバーが 3D で回転し、スポットライトを当てます。
✨ 試してみる
または、position: sticky
を活用したこの積み重ねカードデモです。カードを積み重ねると、すでに行き詰っているカードがスケールダウンされ、深みのある効果が生まれます。最終的には、スタック全体がグループとして視界から消えます。
✨ 試してみる
また、 scroll-driven-animations.style には、この投稿で前述したビュー タイムライン範囲進行状況可視化などのツールのコレクションも表示されています。
スクロールドリブン アニメーションについては、Google I/O 2023 のウェブ アニメーションの新機能でも説明しています。