スクロール タイムラインとビュー タイムラインを使用して、宣言型でスクロール駆動型アニメーションを作成する方法を学びます。
公開日: 2023 年 5 月 5 日
スクロールドリブン アニメーション
スクロール駆動型アニメーションは、ウェブでよく見られる 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) になります。
デモ: 読了進捗インジケーター
このデモでは、読了進捗インジケーターがビューポートの上部に固定されています。ページを下にスクロールすると、進行状況バーが拡大し、ドキュメントの末尾に到達するとビューポートの幅全体を占めるようになります。匿名のスクロール進行状況タイムラインを使用してアニメーションを駆動します。
✨ 実際に試してみる
読了進捗インジケーターは、position: fixed を使用してページの上部に配置されています。合成アニメーションを活用するために、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 プロパティに任意の ID を設定します。値は -- で始まる必要があります。
追跡する軸を調整するには、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 がルート スクロールを追跡し、ページをスクロールすると #progress が X 軸上で 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();
}
間奏: ビュー タイムラインの範囲
デフォルトでは、ビュー タイムラインにリンクしているアニメーションは、タイムラインの範囲全体に適用されます。対象がスクロールポートに入り始めた瞬間から開始し、スクロールポートから完全に離れた時点で終了します。
ビュー タイムラインの特定の部分にリンクすることも可能で、その場合は適用する範囲を指定します。たとえば、対象がスクローラーに入った瞬間にリンクできます。以下の例では、対象がスクロール コンテナに入り始めたら進行状況が 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%',
}
);
以下のツールを使用して、各範囲名の表す内容と、開始位置および終了位置に影響する割合を確認してください。range-start を entry 0% に、range-end を 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 つの異なるアニメーションを実行するのではなく、範囲情報をすでに含むキーフレームのセットを作成することもできます。
@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 プロパティは、スクロール タイムラインとビュー タイムラインの両方で使用できます。
その他のデモとリソース
この記事で取り上げたデモはすべて、scroll-driven-animations.style ミニサイトでご覧いただけます。このウェブサイトには、スクロール駆動型アニメーションで可能なことを示すデモが他にも多数掲載されています。
追加のデモの 1 つが、このアルバム カバーのリストです。各カバーは、中央のスポットライトを浴びながら 3D で回転します。
✨ 実際に試してみる
または、position: sticky を活用したスタッキング カードのデモをご覧ください。カードが積み重なると、すでに固定されているカードが縮小され、奥行きのある効果が生まれます。最終的に、スタック全体がグループとしてスライドして画面外に移動します。
✨ 実際に試してみる
scroll-driven-animations.style にも、この投稿の前半で紹介したビュー タイムライン範囲の進行状況の可視化などのツールがまとめられています。
スクロールドリブン アニメーションについては、Google I/O ’23 のウェブ アニメーションの新機能でも取り上げています。