スクロール タイムラインとビュー タイムラインを使用して、宣言的にスクロールドリブン アニメーションを作成する方法について学びます。
スクロールドリブン アニメーション
スクロールドリブン アニメーションは、ウェブでよく使用される UX パターンです。スクロールドリブン アニメーションは、スクロール コンテナのスクロール位置にリンクされます。つまり、上下にスクロールすると、リンクされたアニメーションが直接応答して前方または後方にスクラブされます。たとえば、視差効果のある背景画像や、スクロールに応じて移動する読み上げインジケーターなどの効果がこれに該当します。
スクロールドリブン アニメーションの類似タイプとして、スクロール コンテナ内の要素の位置にリンクされたアニメーションがあります。たとえば、要素が画面に表示されたときにフェードインするようにできます。
このような効果を実現する従来の方法は、メインスレッドでスクロール イベントに応答することですが、これにより主に次の 2 つの問題が発生します。
- 最新のブラウザでは、スクロールは別のプロセスで行われるため、スクロール イベントは非同期で配信されます。
- メインスレッドのアニメーションはジャンクが発生する可能性があります。
そのため、スクロールと同期して動作する高パフォーマンスのスクロール ドリブン アニメーションを作成することは不可能か、非常に困難です。
Chrome バージョン 115 以降では、宣言型のスクロール駆動アニメーションを有効にするために使用できる新しい API とコンセプト(Scroll Timelines と View Timelines)が導入されています。
これらの新しいコンセプトは、既存の 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-name
と scroll-timeline-axis
を scroll-timeline
ショートカットで組み合わせることができます。次に例を示します。
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();
}
インターメッツォ: タイムラインの範囲を表示する
デフォルトでは、ビュー タイムラインにリンクしているアニメーションは、タイムラインの範囲全体に適用されます。対象がスクロールポートに入る直前から開始し、対象がスクロールポートから完全に離れた時点で終了します。
ビュータイムラインの特定の部分にリンクすることも可能で、その場合は適用する範囲を指定します。たとえば、対象がスクローラーに入った瞬間にリンクできます。以下の例では、対象がスクロール コンテナに入り始めたら進行状況が 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%
に設定し、スクロールバーをドラッグしてアニメーションの結果を確認してみてください。
録画を視聴する
この [タイムラインの範囲を表示] ツールを試していると気付くかもしれませんが、一部の範囲は、2 つの異なる範囲名と範囲オフセットの組み合わせでターゲットに設定できます。たとえば、entry 0%
、entry-crossing 0%
、cover 0%
はすべて同じ領域をターゲットとしています。
range-start と range-end が同じ range-name をターゲットとし、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
として設定されているため、scroll-timeline-root
に接続されます。.subject
要素は--tl
タイムラインを使用します。祖先ツリーを遡り、.parent
の--tl
を見つけます。.parent
の--tl
が.scroller
の--tl
を参照しているため、.subject
は基本的に.scroller
のスクロール進行状況タイムラインを追跡します。
言い換えると、timeline-root
を使用してタイムラインを祖先に移動(ホイスティング)することで、祖先のすべての子にアクセスできるようにできます。
timeline-scope
プロパティは、スクロール タイムラインとビュー タイムラインの両方で使用できます。
その他のデモとリソース
この記事で説明するデモはすべて、scroll-driven-animations.style ミニサイトで確認できます。このウェブサイトには、スクロール ドリブン アニメーションでできることを示すデモが多数用意されています。
追加のデモの一つが、このアルバムカバーのリストです。各カバーは、中央のスポットライトを浴びながら 3D で回転します。
✨ ぜひお試しください
または、position: sticky
を活用したカードの積み重ねのデモもご覧ください。カードが積み重なると、すでに貼り付けられているカードが縮小され、奥行きのある効果が生まれます。最後に、スタック全体がグループとしてスライドして画面外に移動します。
✨ ぜひお試しください
scroll-driven-animations.style には、この投稿の前半で紹介したビューのタイムライン範囲の進行状況のビジュアリゼーションなどのツールが含まれています。
スクロールドリブン アニメーションについては、Google I/O 2023 のウェブ アニメーションの新機能でも説明しています。