スクロール タイムラインとビュー タイムラインを使用して、スクロールドリブン アニメーションを宣言的に作成する方法を学習します。
スクロールドリブン アニメーション
スクロールドリブン アニメーションは、ウェブでよく使用される 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 では、ウェブ アニメーション 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();
}
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%',
}
);
以下の埋め込みツールを使用して、それぞれの範囲名が何を表しているか、割合が開始位置と終了位置にどのように影響するかを確認してください。range-start を entry 0%
、range-end を cover 50%
に設定し、スクロールバーをドラッグしてアニメーションの結果を確認してみてください。
録画を視聴する
この [タイムラインの範囲を表示] ツールを試していると気付くかもしれませんが、一部の範囲は、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-アニメーション.style ミニサイトにあります。ウェブサイトには、スクロールドリブン アニメーションでできることを紹介するデモが他にも多数用意されています。
追加のデモの一つが、このアルバムカバーのリストです。各カバーは、中央のスポットライトを浴びながら 3D で回転します。
✨ ぜひお試しください
または、position: sticky
を活用したカードの積み重ねのデモもご覧ください。カードが積み重なると、すでに貼り付けられているカードが縮小され、奥行きのある効果が生まれます。最後に、スタック全体がグループとしてスライドして画面外に移動します。
✨ ぜひお試しください
scroll-driven-animations.style には、この投稿の前半で紹介したビューのタイムライン範囲の進行状況のビジュアリゼーションなどのツールが含まれています。
スクロールドリブン アニメーションについては、Google I/O 2023 のウェブ アニメーションの新機能でも説明しています。