JavaScriptなしでスクロールに応じてCSSアニメーションをトリガーする
JavaScriptなしでスクロール連動のCSSアニメーションを作成。構文やトリガー動作、境界の設定、IntersectionObserverによる代替策を解説します。
スクロールトリガーアニメーションでは、通常のCSSアニメーションをいつ開始するかをスクロールで決めます。要素が指定したスクロール位置に到達すると、アニメーションはそれ自身のクロックで再生されます。timeline-triggerプロパティとanimation-triggerプロパティで宣言するだけで、IntersectionObserverは必要ありません。
「スクロールでフェードイン」のコードは、どのプロジェクトでもほぼ同じ形をしています。オブザーバー、しきい値、クラスの切り替え、そして誰かが書き忘れるunobserve()の呼び出しです。この処理を、いまではCSSで実現できます。
本記事では、構文、完全なリビール(表示)アニメーションの例、3種類のトリガー動作、スクロール境界の選び方、そしてChromiumのみが対応している現状でこの機能を安全にリリースする方法を解説します。
重要なポイント
- スクロールトリガーアニメーションには2つの宣言が必要です。
timeline-triggerはトリガーに名前を付け、トリガーを有効化するスクロール範囲を設定します。animation-triggerは、アニメーションがどのトリガーに従い、トリガーが発火したときに何をするかを指定します。 - 範囲
entry 100% exit 0%は、要素がスクロールポート内に完全に収まっている間だけトリガーを有効化します。Chromeのドキュメントでは、これをcontainキーワードと同等として扱っています。 play-forwards play-backwardsは、進入時にアニメーションを再生し、退出時に逆再生します。Animation Triggersのドラフトで定義されているplay-onceは、アニメーションを一度だけ実行します。- トリガーされたアニメーションは独自のdurationとイージングを保持するため、速くスクロールしても再生速度は上がりません。
- Chromeのサポート情報では、スクロールトリガーアニメーションの対応ブラウザとしてChromium系ブラウザのみが記載されています。そのため、コンテンツを非表示にする処理は
@supports内に記述し、IntersectionObserverによるフォールバックを残しておきましょう。
スクロール駆動アニメーションとスクロールトリガーアニメーションの違いとは?
スクロール駆動アニメーションは、アニメーションの進行度をスクロール位置に結び付けます。一方、スクロールトリガーアニメーションは、通常のアニメーションをいつ開始するかを決めるためだけにスクロールを利用します。前者のモデル、つまりscroll()とview()を使ったanimation-timelineについては、CSSだけでスクロールアニメーションを作成する方法のガイドで解説しています。そこではタイムラインがクロックの代わりになるため、スクロールを戻すとアニメーションが逆再生されます。本記事で扱うのはtimeline-triggerとanimation-triggerです。これらは、要素が指定したスクロール範囲に到達した時点で、独自のdurationを持つ通常のアニメーションを開始します。
読了進捗バーやパララックスを実装したい場合は、CSSスクロール進行度アニメーションを使いましょう。カードが表示された時点で400msかけてフェードインさせたい場合は、トリガーを使います。ChromeはGoogle I/O 2026のWeb UIまとめで両者を別々の機能として挙げ、トリガーをIntersectionObserverをCSSで置き換える手段として説明しています。簡単に言えば、トリガーは「この要素が表示されたらクラスを追加する」処理のCSS版です。
スクロールトリガーによるフェードインの例
スクロールトリガーによるフェードインには、3つの要素が必要です。@keyframesルール、forwardsフィルを指定したanimation、そしてアニメーションをビュータイムラインに結び付けるトリガーです。構文はChromeによるスクロールトリガーアニメーションの発表で定義されています。主なポイントは、トリガーとアニメーションを別々の要素に配置できること、そしてトリガーがスクロールまたはビューの進行度タイムラインをソースとして使うことです。
<section class="reveal">
<div class="reveal-content">
<h2>Shipping notes</h2>
<p>This block fades and slides in once it is fully on screen.</p>
</div>
</section>
@keyframes reveal {
to {
opacity: 1;
translate: 0 0;
}
}
@supports (timeline-trigger-name: --t) {
@media (prefers-reduced-motion: no-preference) {
.reveal {
timeline-trigger: --reveal view() entry 100% exit 0%;
trigger-scope: --reveal;
}
.reveal-content {
opacity: 0;
translate: 0 2rem;
animation: reveal 400ms ease-out forwards;
animation-trigger: --reveal play-forwards;
}
}
}
仕組みは次のとおりです。
- トリガーはsectionに、動きはその子要素に配置しています。 スライドする要素と、表示状態を計測する要素が異なるため、
translateによるオフセットがトリガーの境界を動かすことはありません。 entry 100% exit 0%は、sectionがスクロールポート内に完全に収まった時点でトリガーを有効化します。play-forwardsは、トリガーが有効化されるたびにアニメーションを開始します。forwardsフィルにより、再生後は最終キーフレームの状態が保持されます。- 非表示の初期状態は機能クエリの内側に記述しています。 非対応ブラウザでは
opacity: 0が適用されないため、コンテンツは表示されたままになります。 trigger-scopeは、各sectionの--revealという名前をローカルに保ちます。Chrome 146のリリースノートにあるとおり、デフォルトではanchor-nameと同様に、ページ上のどの要素からもトリガー名を参照できてしまいます。
play-forwardsのみを指定した場合、範囲から出ても何も起こりません。スクロールして離れても、要素は終了状態のままです。再び完全に表示されると、トリガーが再度有効化され、アニメーションがもう一度再生されます。この動作を変更する方法を次のセクションで説明します。
トリガーアクションの選択
animation-triggerには、トリガー名に続けて最大2つのアクションを指定できます。1つ目はトリガーが有効化されたときに、2つ目は無効化されたときに実行されます。どのアクションを選ぶかによって、再進入時と退出時の動作が決まります。
| 求める動作 | animation-triggerの値 | 注意点 |
|---|---|---|
| 再進入のたびに再生する | --reveal play-forwards | 初期状態へ目に見えて戻ることがある |
| 退出時に逆再生する | --reveal play-forwards play-backwards | 逆再生後、要素はベーススタイルで表示される |
| 一度だけ実行して状態を維持する | --reveal play-once | ドラフトのキーワードであり、変更される可能性がある |
animation-trigger: --reveal play-forwards play-backwards; /* reverses on exit */
animation-trigger: --reveal play-once; /* runs once; Animation Triggers draft keyword */
forwardsフィルは、play-forwardsの完了後にtoキーフレームの状態を保持します。しかし、play-backwardsの完了後には何も保持しません。その時点でアニメーションの適用が止まり、要素はベーススタイルに戻ります。この例ではキーフレームがtoのみを定義しているため、ベーススタイルは非表示の初期状態(opacity: 0; translate: 0 2rem)であり、結果として要素は非表示のままになります。キーフレームでベーススタイルと異なるfromの状態を設定している場合は、逆再生後もその状態が保持されるようbothを使ってください。play-onceはChromeの発表ではなく、CSS Animation Triggers Level 1 Editor’s Draftに由来するものです。利用する前に、対象とするChromeのビルドで動作を確認してください。これは、最初の交差後にunobserve()を呼び出す処理に最も近い動作です。
アクティベーション範囲とアクティブ範囲
スクロールトリガーでは、アクティベーション範囲がトリガーの発火タイミングを決めます。スラッシュの後に記述するオプションのアクティブ範囲は、トリガーが有効な状態をどれだけ維持するかを決めます。Chromeでは、アクティブ範囲がアクティベーション範囲を包含している必要があります。Chromeによると、entry 100% exit 0%はcontainと、entry 0% exit 100%はcoverと同等であるため、次の2行は同じ意味になります。
timeline-trigger: --reveal view() entry 100% exit 0% / entry 0% exit 100%;
timeline-trigger: --reveal view() contain / cover;
この設定では、要素が完全に表示された時点でトリガーが有効になり、要素がまったく見えなくなるまで有効な状態が続きます。これはplay-backwardsを使う場合に重要です。逆再生のアニメーションが、要素が画面外に出るまで待機するためです。
適切なスクロール境界の選び方
適切なスクロールトリガーの範囲は、要素のサイズと、ビューポートを覆う固定UIの有無によって異なります。
| 状況 | 範囲 | 理由 |
|---|---|---|
| 小さなカードや見出し | entry 100% exit 0%(contain) | 要素全体が表示された時点で発火する |
| 縦に長いセクション | entry 25% exit 0% | 縦に長い要素ではcontainの発火が遅れる場合がある |
| 高さ4remのスティッキーヘッダーの下 | view(block 4rem 0) | ヘッダーが要素を隠してしまうのを防ぐ |
/* Tall element: fire when a quarter has entered */
timeline-trigger: --reveal view() entry 25% exit 0%;
/* 4rem sticky header: start (top) inset first */
timeline-trigger: --reveal view(block 4rem 0) entry 100% exit 0%;
view()では、開始側のインセットを終了側のインセットより先に記述します。縦方向のブロック軸では開始側が上端になるため、上部ヘッダーの高さを先に指定します。
ブラウザサポートとフォールバック
ChromeのGoogle I/O 2026のWeb UIまとめでは、スクロールトリガーアニメーションのサポート状況として、Chrome 146およびEdge 146が対応、FirefoxとSafariは非対応と記載されています。Chromeの2025年12月の発表ではChrome 145で導入予定とされていましたが、実際にはChrome 146(安定版リリースは2026年3月10日)で提供されました。Chrome 146ベータ版の記事では、同じリリースにtrigger-scopeも含まれていることが示されています。この機能全体をプログレッシブエンハンスメントとして扱いましょう。
例で使った@supportsのラッパーがこれに対応します。非対応ブラウザはブロック全体をスキップし、コンテンツを静的に描画します。他のブラウザでも同じ動きを実現したい場合は、既存のIntersectionObserverによるクラス切り替えを機能クエリの外に残し、CSS.supports()がfalseを返すときだけ実行するようにします。AOSのような依存ライブラリを使わずにこのフォールバックを構築する方法は、アニメーションライブラリをネイティブWeb APIで置き換えるの記事で解説しています。
prefers-reduced-motion: no-preferenceクエリにより、動きの抑制を希望するユーザーにはコンテンツがすぐに表示されます。複数の要素を時間差で表示させたい場合は、それらのanimation-triggerを共通の1つのトリガーに向け、それぞれに異なるdelayを設定します。
まとめ
スクロールトリガーアニメーションは、スクロールでフェードインするパターンをカバーします。通常のアニメーションをいつ開始するかをCSSが決め、監視はブラウザが行います。まずは上記の例から始めましょう。デザインに合ったアクション(再生の繰り返し、逆再生、一度だけ実行)を選び、縦に長い要素やスティッキーヘッダーに合わせて範囲を調整してください。そして、対応ブラウザではオブザーバーのコードを取り除き、それ以外のブラウザ向けには残しておきましょう。
よくある質問
スクロールトリガーアニメーションとscroll-stateコンテナクエリの違いは何ですか?
scroll-stateコンテナクエリは、コンテナがブラウザの管理するスクロール状態にある間、スタイルを適用します。スクロールトリガーアニメーションは、要素がビュータイムラインまたはスクロールタイムライン上の範囲を通過したときに、キーフレームアニメーションを開始します。Chrome 133ではstuck、snapped、scrollableの各状態が提供され、Chrome 144では直近のスクロール方向を追跡するscrolledが追加されました。固定された時点でスタイルを変えるスティッキーヘッダーにはscroll-stateクエリを、リビール効果にはトリガーを使いましょう。
スクロールトリガーアニメーションは、IntersectionObserverによるリビールより高速ですか?
Chromeのリリースノートによると、インタラクションがCSSで記述されるため、ブラウザは処理をワーカースレッドに委ねることができます。IntersectionObserverによるリビールは、コールバックとクラスの切り替えをメインスレッドで実行するため、重いスクリプト処理によって遅延する可能性があります。ただし、Chromeはこのオフロードを計測結果ではなく設計上の利点として説明しています。性能向上を前提にせず、切り替えの前後で自分のページをプロファイリングしてください。
スクロールトリガーで、Web Animations APIで作成したアニメーションを制御できますか?
はい、できます。Chrome 146のリリースノートによると、この機能にはJavaScriptインターフェースが用意されており、CSSアニメーションだけでなくWeb Animations APIで作成したアニメーションもトリガーで制御できます。インターフェース名はCSS Animation Triggers Level 1 Editor's Draftで定義されていますが、今後変更される可能性があります。利用する前に、ドラフトと対象のChromeビルドを確認してください。
スクロールトリガーを追加したら要素が非表示のままになるのはなぜですか?
非表示の初期状態は機能クエリが一致した時点で適用されるため、要素はトリガーが有効化されるまでopacity 0のままになります。よくある原因は、timeline-triggerの名前とanimation-triggerの名前のスペルの不一致、アニメーション対象の要素がtrigger-scopeで設定されたサブツリーの外にあること、または要素が到達しない範囲を指定していることです。まず名前を確認し、次にアニメーション対象の要素がスコープを設定した要素の内側にあることを確認してください。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k