CSS でスクロールドリブン アニメーションを使ってみる

1. 始める前に

スクロールドリブン アニメーションを使用すると、スクロール コンテナのスクロール位置に基づいてアニメーションの再生を制御することができます。つまり、上下にスクロールすると、アニメーションが前方または後方にスクラブされます。また、スクロールドリブン アニメーションを使用すると、スクロール コンテナ内の要素の位置に基づいてアニメーションを制御することもできます。これにより、視差効果のある背景画像、スクロールの進行状況バー、ビューに入ったときに出現する画像など、面白い視覚効果を作成することができます。

Chrome 114 では、宣言型のスクロールドリブン アニメーションを簡単に作成できる一連の JavaScript クラスと CSS プロパティが新たにサポートされました。これらの新しい API は、既存の Web Animations API や CSS Animations API と一緒に機能します。

この Codelab では、CSS を使用してスクロールドリブン アニメーションを作成する方法について説明します。この Codelab を完了すると、scroll-timelineview-timelineanimation-timelineanimation-range など、この優れた機能で新たに導入された多くの CSS プロパティについて理解することができます。

学習内容

  • CSS でスクロール タイムラインを使用して視差効果のある背景を作成する方法
  • CSS でスクロール タイムラインを使用して進行状況バーを作成する方法
  • CSS でビュー タイムラインを使用して画像の出現効果を作成する方法
  • CSS でさまざまな範囲のビュー タイムラインを使用する方法

必要なもの

次のいずれかの組み合わせが必要です。

  • ChromeOS、macOS、または Windows 版 Chrome の最新バージョン(114 以降)
  • HTML に関する基本的な知識
  • CSS に関する基本的な知識(特に CSS でのアニメーションの処理)

2. 設定する

このプロジェクトに必要なすべてのコードは GitHub リポジトリにあります。まず、コードのクローンを作成して、お好みの開発環境で開きます。

  1. 新しいブラウザタブを開いて、https://github.com/googlechromelabs/io23-scroll-driven-animations-codelab に移動します。
  2. リポジトリのクローンを作成します。
  3. お使いの IDE でコードを開きます。
  4. npm install を実行して、依存関係をインストールします。
  5. npm start を実行して、http://localhost:3000/ に移動します。
  6. npm がインストールされていない場合は、Chrome で src/index.html ファイルを開きます。

3. アニメーションのタイムラインについて学ぶ

デフォルトでは、要素に適用されているアニメーションはドキュメント タイムラインで実行されます。つまり、ページが読み込まれると、時間の経過に合わせてアニメーションが再生されます。これはアニメーション タイムラインのデフォルトですが、これまでは、この他に利用できるアニメーション タイムラインはありませんでした。

スクロールドリブン アニメーションでは、新しい次の 2 種類のタイムラインにアクセスできます。

  • スクロール進行状況タイムライン
  • ビュー進行状況タイムライン

CSS では、animation-timeline プロパティを使用して、これらのタイムラインをアニメーションに適用します。では、この新しいタイムラインがどのようなもので、それぞれどのような違いがあるのか見てみましょう。

スクロール進行状況タイムライン

スクロール進行状況タイムラインは、特定の軸でのスクロール コンテナのスクロール位置の進行状況とリンクするアニメーション タイムラインで、スクロールポート、スクローラーとも呼ばれています。これは、スクロール範囲内の位置をタイムライン上で進行状況の割合に変換します。

スクロールの開始位置の進行状況は 0%、終了位置は 100% です。以下の例では、スクローラーを下にスクロールすると、進行状況が 0%~100% でカウントされます。

ビュー進行状況タイムライン

このタイプのタイムラインは、スクロール コンテナ内の特定の要素の相対的な進行状況にリンクしています。スクロール進行状況タイムラインと同様に、スクローラーのスクロール オフセットが追跡されます。スクロール進行状況タイムラインとは異なり、ここでは、スクローラー内の対象の相対的な位置によって進行状況が決まります。これは、スクローラーで要素がどのくらい表示されているかを追跡する IntersectionObserver に相当するものです。要素がスクローラー内に表示されていなければ、要素は交差していません。わずかでもスクローラー内に表示されていれば、要素は交差しています。

ビュー進行状況タイムラインは、対象がスクローラーとの交差を開始した瞬間から開始し、スクローラーとの交差を停止すると終了します。以下の例では、対象がスクロール コンテナに入った時点で進行状況が 0% から開始し、対象がスクロール コンテナから離れた時点で 100% になります。

デフォルトでは、ビュー進行状況タイムラインにリンクしているアニメーションは、範囲全体に適用されます。対象がスクロールポートに入った瞬間から開始し、スクロールポートから離れた時点で終了します。

ビュー進行状況タイムラインの特定の部分にリンクすることも可能で、その場合は適用する範囲を指定します。たとえば、対象がスクローラーに入った瞬間にリンクできます。以下の例では、対象がスクロール コンテナに入り始めたら進行状況が 0% で開始し、完全に交差した瞬間に 100% になります。

ターゲットに設定可能なビュー タイムラインの範囲は、covercontainentryexitentry-crossingexit-crossing です。これらの範囲については、この Codelab の後半で説明しますが、すぐに確認したい場合は、https://goo.gle/view-timeline-range-tool にあるツールを使って、それぞれの範囲が表す内容をご覧ください。

4. 視差効果のある背景を作成する

まず最初に、ページのメインの背景画像に視差効果を追加してみましょう。ページを下にスクロールすると背景画像が動きます。ただし、速度は一定ではありません。この設定を行うには、スクロール進行状況タイムラインを使用します。

実装は 2 段階で行います。

  1. 背景画像の位置を動かすアニメーションを作成します。
  2. アニメーションをドキュメントのスクロール進行状況にリンクします。

アニメーションを作成する

  1. アニメーションを作成するには、通常のキーフレーム セットを使用します。その中で、背景の位置を 0% から 100% で垂直に移動します。

src/styles.css

@keyframes move-background {
  from {
    background-position: 50% 0%;
  }
  to {
    background-position: 50% 100%;
  }
}
  1. では、これらのキーフレームを本文要素に適用します。

src/styles.css

body {
  animation: 1s linear move-background;
}

このコードでは、move-background アニメーションが本文要素に追加されています。その animation-duration プロパティは 1 秒に設定され、linear イージングが使用されます。

スクロール進行状況タイムラインを使用する最も簡単な方法は scroll() 関数を使用する方法です。これにより、匿名のスクロール進行状況タイムラインを作成し、animation-timeline プロパティの値として設定できます。

scroll() 関数は、<scroller> 引数と <axis> 引数を受け入れます。

<scroller> 引数で使用できる値は次のとおりです。

  • nearest。最も近い祖先スクロール コンテナを使用します(デフォルト)。
  • root。スクロール コンテナとしてドキュメント ビューポートを使用します。
  • self。要素自身をスクロール コンテナとして使用します。

<axis> 引数で使用できる値は次のとおりです。

  • block。スクロール コンテナのブロック軸で進行状況を測定します。
  • inline。スクロール コンテナのインライン軸で進行状況を測定します。
  • y。スクロール コンテナの Y 軸で進行状況を測定します。
  • x。スクロール コンテナの X 軸で進行状況を測定します。

アニメーションをブロック軸のルート スクローラーにリンクする場合、scroll() に渡す値は rootblock: scroll(root block) です。

  • body の animation-timeline プロパティの値として scroll(root block) を設定します。さらに、animation-duration は秒単位でで表すべきではないので、期間を auto に設定します。animation-duration を指定しない場合、デフォルトは auto になります。

src/styles.css

body {
  animation: linear move-background;
  animation-duration: auto;
  animation-timeline: scroll(root block);
}

ルート スクローラーが body 要素の最も近い親スクローラーになることもあるので、nearest の値を使用することもできます。

src/styles.css

body {
  animation: linear move-background;
  animation-duration: auto;
  animation-timeline: scroll(nearest block);
}

nearestblock はデフォルト値で、省略することもできます。その場合、コードは次のようになります。

src/styles.css

body {
  animation: linear move-background;
  animation-duration: auto;
  animation-timeline: scroll();
}

変更を確認する

正常に処理されていれば、次のようになっているはずです。

そうでない場合は、コードの solution-step-1 ブランチを調べてください。

5. 画像ギャラリーに進行状況バーを作成する

ページで現在表示している写真を示すには、水平カルーセルに進行状況バーが必要です。

カルーセルのマークアップは次のようになります。

src/index.html

<div class="gallery">
  <div class="gallery__scrollcontainer" style="--num-images: 3;">
    <div class="gallery__progress"></div>
    <div class="gallery__entry">
      ...
    </div>
    <div class="gallery__entry">
      ...
    </div>
    <div class="gallery__entry">
      ...
    </div>
  </div>
</div>

進行状況バーのキーフレームは、次のようにすでに配置されています。

src/styles.css

@keyframes adjust-progress {
  from {
    transform: scaleX(calc(1 / var(--num-images)));
  }
  to {
    transform: scaleX(1);
  }
}

このアニメーションは、スクロール進行状況タイムラインの gallery__progress 要素に適用する必要があります。前のステップで見たように、これを有効にするには、scroll() 関数で匿名のスクロール進行状況タイムラインを作成します。

src/styles.css

.gallery__progress {
  animation: linear adjust-progress;
  animation-duration: auto;
  animation-timeline: scroll(nearest inline);
}

また、名前付きのスクロール進行状況タイムラインを定義する方法もあります。これはやや長くなりますが、親スクローラーやルート スクローラーをターゲットにしている場合や、ページで複数のタイムラインを使用する場合には便利な方法です。この方法では、任意の名前でスクロール進行状況タイムラインを識別できます。

要素に名前付きスクロール進行状況タイムラインを作成するには、スクロール コンテナの scroll-timeline-name CSS プロパティに任意の値を設定します。

ギャラリーは水平方向にスクロールするので、scroll-timeline-axis プロパティも設定する必要があります。使用可能な値は、scroll()<axis> 引数と同じです。

最後に、アニメーションをスクロール進行状況タイムラインにリンクします。アニメーションを再生する要素の animation-timeline プロパティに、scroll-timeline-name で使用した ID と同じ値を設定します。

  • 次のものを含むように styles.css ファイルを変更します。

src/styles.css

.gallery__scrollcontainer {
  /* Create the gallery-is-scrolling timeline */
  scroll-timeline-name: gallery-is-scrolling;
  scroll-timeline-axis: inline;
}

.gallery__progress {
  animation: linear adjust-progress;
  animation-duration: auto;
  /* Set gallery-is-scrolling as the timeline */
  animation-timeline: gallery-is-scrolling;
}

変更を確認する

正常に処理されていれば、次のようになっているはずです。

そうでない場合は、コードの solution-step-2 ブランチを調べてください。

6. スクロールポートに出入りするときにギャラリー画像をアニメーション表示する

匿名のビュー進行状況タイムラインを設定する

ギャラリー画像がビューに入ったときにフェードインすると見栄えが良くなります。この目的でビュー進行状況タイムラインを使用できます。

ビュー進行状況タイムラインを作成するには、view() 関数を使用します。使用できる引数は <axis><view-timeline-inset> です。

  • <axis> はスクロール進行状況タイムラインと同じで、追跡する軸を定義します。
  • <view-timeline-inset> を使用すると、オフセット(正または負)を指定して、要素がビュー内にある(またはない)とみなされる境界を調整できます。
  • キーフレームはすでに配置されているので、あとはこれらを適用するだけです。各 .gallery__entry 要素にビュー進行状況タイムラインを作成します。

src/styles.css

@keyframes animate-in {
  from {
    opacity: 0;
    clip-path: inset(50% 0% 50% 0%);
  }
  to {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }
}

.gallery__entry {
  animation: linear animate-in;
  animation-duration: auto;
  animation-timeline: view(inline);
}

ビュー進行状況タイムラインの範囲を制限する

CSS を保存してページを読み込むと、要素がフェードインしますが、何かが消えているように見えます。要素が完全にビューの外側にあるときは不透明度 0 ですが、完全に終了したときには不透明度が 1 になります。

これは、ビュー進行状況タイムラインのデフォルトの範囲が全範囲であることが原因です。これは cover 範囲ともいわれています。

  1. 対象の entry 範囲のみをターゲットにする場合は、animation-range CSS プロパティを使用して、アニメーションの実行を制限します。

src/styles.css

.gallery__entry {
  animation: linear fade-in;
  animation-duration: auto;
  animation-timeline: view(inline);
  animation-range: entry 0% entry 100%;
}

これで、アニメーションが entry 0%(対象がスクローラーに入るとき)から entry 100%(対象全体がスクローラーに入ったとき)まで実行されるようになりました。

設定可能なビュー タイムラインの範囲は次のとおりです。

  • cover。ビュー進行状況タイムラインの全範囲を表します。
  • entry。プリンシパル ボックスがビュー進行状況の表示範囲に入っている途中の状態の範囲を表します。
  • exit。プリンシパル ボックスがビュー進行状況の表示範囲から出ている途中の状態の範囲を表します。
  • entry-crossing。プリンシパル ボックスが終了境界エッジと交差している状態の範囲を表します。
  • exit-crossing。プリンシパル ボックスが開始境界エッジと交差している状態の範囲を表します。
  • contain。プリンシパル ボックスがスクロール ポート内のビュー進行状況の表示範囲に完全に含まれているか、完全にカバーしている状態の範囲を表します。これは、対象がスクローラーよりも長いか短いかによって変わります。

https://goo.gle/view-timeline-range-tool にあるツールを使用して、各範囲の表す内容と、開始位置および終了位置に影響する割合を確認してください。

  1. ここでは、開始範囲と終了範囲が同一で、デフォルトのオフセットを使用しているため、animation-range は 1 つのアニメーション範囲名を表しています。

src/styles.css

.gallery__entry {
  animation: linear animate-in;
  animation-duration: auto;
  animation-timeline: view(inline);
  animation-range: entry;
}
  • 画像がスクローラーから出るときにフェードアウトするには、animate-in アニメーションと同じ方法を使用できます。ただし、ターゲットにする範囲は異なります。

src/styles.css

@keyframes animate-out {
  from {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }
  to {
    opacity: 0;
    clip-path: inset(50% 0% 50% 0%);
  }
}

.gallery__entry {
  animation: linear animate-in, linear animate-out;
  animation-duration: auto;
  animation-timeline: view(inline);
  animation-range: entry, exit;
}

animate-in キーフレームが entry 範囲に適用され、animate-out キーフレームが exit 範囲に適用されます。

変更を確認する

正常に処理されていれば、次のようになっているはずです。

そうでない場合は、コードの solution-step-3 ブランチを調べてください。

7. いずれかのキーフレーム セットを使用して、スクロールポートに出入りするときにギャラリー画像をアニメーション表示する

1 つのキーフレーム セットを使用する場合

2 つのアニメーションを別々の範囲に適用するのではなく、範囲情報をすでに含むキーフレームのセットを作成することもできます。

このキーフレームは次のようになります。

@keyframes keyframes-name {
  range-name range-offset {
    ...
  }
  range-name range-offset {
    ...
  }
}
  1. 次のように、フェードイン キーフレームとフェードアウト キーフレームを組み合わせます。

src/styles.css

@keyframes animate-in-and-out {
  entry 0% {
    opacity: 0;
    clip-path: inset(50% 0% 50% 0%);
  }
  entry 90% {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }

  exit 10% {
    opacity: 1;
    clip-path: inset(0% 0% 0% 0%);
  }
  exit 100% {
    opacity: 0;
    clip-path: inset(50% 0% 50% 0%);
  }
}
  1. キーフレームに範囲情報が存在しているので、これ以上 animation-range を指定する必要はありません。キーフレームを animation プロパティとして適用します。

src/styles.css

.gallery__entry {
  animation: linear animate-in-and-out both;
  animation-duration: auto;
  animation-timeline: view(inline);
}

変更を確認する

正常に処理されていれば、前のステップと同じ結果になるはずです。そうでない場合は、コードの solution-step-4 ブランチを調べてください。

8. 完了

これで、この Codelab は終わりです。ここでは、CSS でスクロール進行状況タイムラインとビュー進行状況タイムラインを作成する方法を学びました。

詳細

リソース: