はじめに — なぜ今 animation-trigger なのか

スクロールに応じたアニメーションを実装するとき、これまでは Intersection Observer を使うのが定石でした。ただし、アニメーションのロジックと監視ロジックが分離してしまうという課題があります。observer のコールバックでクラスを付与し、そのクラスに対して CSS でアニメーションを定義する——「スクロールしたらフェードイン」程度の要件に対しては、やや冗長な構成です。

animation-trigger は、この配線を CSS 側に完全に取り込みます。トリガーの宣言、有効範囲、再生・逆再生のアクションまで、すべて CSS プロパティとして表現できます。ただし、重要な前提があります。

⚠️ 試験的な機能です。 執筆時点では Chrome 145+ のみで動作し、仕様は Editor's Draft 段階です。本番投入の前に必ずブラウザ対応状況を確認してください。

本記事では timeline-trigger と animation-trigger を組み合わせ、スクロールトリガーアニメーションを実装する手順を実践コードとともに整理します。(根拠資料: CSS-Tricks - animation-trigger)

スクロール駆動型とスクロールトリガー型の違い

混同しやすいポイントなので、先に整理しておきます。

  • Scroll-driven(スクロール駆動型): スクロール位置とアニメーションの進行度が 1:1 で同期します。スクロールを止めればアニメーションも止まります。「開始の瞬間」という概念が存在しません。
  • Scroll-triggered(スクロールトリガー型): トリガーが on/off の二値状態を持ち、条件が満たされた瞬間にアニメーションが 発火します。以後は通常の CSS アニメーションとして独立して再生されます。

animation-trigger は後者、すなわち トリガー方式のためのプロパティです。

Developer inspecting CSS animation-trigger property in Chrome DevTools on a web layout Coding Session Visual

基本構文と実践コード

1) timeline-trigger でトリガーを定義する

まず「いつ発動するか」を定義するトリガーを作成します。timeline-trigger ショートハンドで 1 行にまとめるのが一般的です。

/* トリガー要素: ビューポートに完全に入ったら有効化 */
.trigger {
  /* name / source / activation-range / active-range */
  timeline-trigger: --trigger scroll() contain / cover;
}
  • --trigger: animation-trigger から参照する カスタムダッシュ識別子
  • scroll(): スクロール進行タイムラインをソースとして使用
  • contain: 要素が 完全に表示されたとき トリガー ON
  • cover: 要素の 一部でも表示されている間 トリガーを維持

💡 値の順序は入れ替えできません。background や border のショートハンドとは異なり、timeline-trigger は 順序に意味を持ちます。

2) animation-trigger でアニメーションを接続する

次に、実際にアニメーションを再生する要素へトリガーを紐づけます。

.text {
  /* --trigger が有効化されたらアニメーションを再生 */
  animation-trigger: --trigger play;
  animation: fade 0.6s ease-out;
}

@keyframes fade {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: translateY(0); }
}

3) アクションの組み合わせで挙動を変える

同じトリガーでも アクションを変えるだけで全く異なる UX が得られます。ここが本プロパティの真価です。

/* 進入時に正方向、退出時に逆方向 */
.card-a { animation-trigger: --trigger play-forwards play-backwards; }

/* 1 回のみ再生、以降はトリガーを無視 */
.card-b { animation-trigger: --trigger play-once; }

/* 進入時にリセットして再生(スクロールを繰り返すたびに再登場) */
.card-c { animation-trigger: --trigger replay; }

/* 進入時に停止、退出時に原位置へ戻す */
.card-d { animation-trigger: --trigger pause reset; }

主なアクションは以下のとおりです。

アクション動作
play現在の再生速度で再生
play-forwards正方向に再生
play-backwards逆方向に再生
play-once初期状態または一時停止状態からのみ 1 回再生
pause現在位置で停止
reset進行度を 0 に戻して停止
replay進行度を 0 に戻して即座に再生
none何もしない

4) トリガーとアニメーションは別要素に配置できる

実務で特に有用なポイントです。親要素にトリガーを定義し、複数の子要素に animation-trigger を適用すれば、親がビューポートに入った瞬間に子要素が一斉にアニメーションします。

.section {
  timeline-trigger: --section scroll() contain / cover;
}

.section .item {
  animation-trigger: --section play;
  animation: fade 0.5s ease-out both;
}

カード UI を順次表示させる場合に、Intersection Observer を複数生成する必要がなくなります。

Laptop screen showing scroll-triggered fade-in CSS demo with timeline-trigger code Development Concept Image

注意点と実務判断

1) トリガースコープは既定でグローバル

同名のトリガーを複数の要素で定義した場合、カスケード上で後から来る要素が優先されます。これを避けたい場合は trigger-scope で特定の DOM サブツリーにトリガーを閉じ込めます。

.section {
  trigger-scope: --section; /* このサブツリー内でのみ有効 */
}

2) active-range は activation-range を含む必要がある

activation-range はトリガーが ON になる条件、active-range はトリガーが維持される条件です。active-range が activation-range を含んでいない場合、トリガーは ON になりません。 ここでハマるケースが多いです。

/* ❌ 誤った例: active が activation を含んでいない */
timeline-trigger: --bad scroll() cover / contain;

/* ✅ 正しい例 */
timeline-trigger: --good scroll() contain / cover;

3) 日本の開発現場における現実的な適用

率直に申し上げると、日本の受託開発やコーポレートサイトでは現時点での本番適用は難しいです。理由は明確です。

  • Chrome 145+ 限定のため、Safari(iOS)、Firefox、および依然としてシェアの高い旧版 Chrome では動作しません。
  • 日本のモバイル Web では iOS Safari のシェアが依然として高いため、純粋な CSS のみで実装すると iOS でアニメーションが表示されません。
  • 仕様が Editor's Draft のため、構文が変更される可能性があります。

実務で使える折衷案は以下のとおりです。

/* 既定は静的状態、対応ブラウザでのみトリガーを有効化 */
.text {
  opacity: 1; /* 既定では表示 */
}

@supports (animation-trigger: none) {
  .text {
    opacity: 0;
    animation-trigger: --trigger play;
    animation: fade 0.6s ease-out forwards;
  }
}

@supports で囲むことで、未対応ブラウザではそのままコンテンツが表示され、対応ブラウザでのみアニメーションが適用されます。フォールバックの観点で最も安全なパターンです。

4) アクセシビリティの観点で必ず対応すべきこと

スクロールトリガーアニメーションは モーション感受性の高いユーザー に負担をかけます。prefers-reduced-motion への対応は任意ではなく必須です。

@media (prefers-reduced-motion: reduce) {
  .text, .section .item {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

セマンティックなマークアップとアクセシビリティを同時に考慮する考え方については、CSS で完璧なパイチャートを作る: セマンティクスとアクセシビリティを考慮した実践ガイド でも詳しく扱っていますので、あわせてご参照ください。

Frontend designer comparing scroll-driven and scroll-triggered animation behaviors side by side

まとめ — 今学んでおけば 1〜2 年後に効いてくる

animation-trigger は、「JavaScript なしでスクロールアニメーションを CSS だけで」 という長年の課題を解決するプロパティです。現時点で本番投入はできませんが、方向性は明確です。:has()、コンテナクエリ、@scope と同じく、「CSS が JavaScript の領域を飲み込んでいく流れ」 の延長線上にあります。

次のステップ学習の方向性

  1. timeline-trigger ショートハンドの構文を習得する — 順序に意味があるため、反復練習が必要です。
  2. Scroll-driven animations と比較検証する — Chrome チームの timeline ranges visualizer で両方式の違いを視覚的に確認してください。
  3. @supports フォールバックパターンを定着させる — 実験的 CSS を扱う際の基本です。
  4. prefers-reduced-motion 対応を習慣化する — アクセシビリティは後付けではなく、最初から組み込むものです。

あわせて読みたい記事

実験的な仕様を先に学んでおくことは、「標準化されたときに既に知っている人」 になるための最も安価な投資です。今はデモで遊んでおくだけでも十分です。

本コンテンツは、信頼性の高い情報源をもとにAIツールを活用して作成され、編集者によるレビューを経て公開されています。専門家によるアドバイスの代替となるものではありません。