メインコンテンツまでスキップ
  • Created:
  • Updated:
  • Author:
    Takeshi Takatsudo

スクロール駆動アニメーション

問題

プログレスバー、パララックス背景、要素のリビールアニメーションなどのスクロール連動エフェクトは、従来JavaScriptの scroll イベントリスナーやIntersection Observerのコールバックを必要としていました。これらのアプローチはメインスレッドで実行され、負荷がかかるとカクつきの原因になり、複雑さも増します。AIエージェントはCSSネイティブのScroll-Driven Animations APIをほとんど提案しませんが、このAPIはJavaScriptゼロでスクロール位置に連動したパフォーマンスの高いコンポジタースレッドアニメーションを提供します。

解決方法

CSS Scroll-Driven Animations を使用すると、@keyframes アニメーションを時間ではなくスクロールの進行状況に接続できます。2種類のタイムラインが利用可能です:

  • scroll() — コンテナのスクロール位置を追跡します(0% = 先頭、100% = 完全にスクロール済み)。グローバルなプログレスバーやパララックスに使います。
  • view() — 要素がビューポートに入ってから出るまでの可視性を追跡します。リビールアニメーションや要素レベルのエフェクトに使います。

どちらのタイムラインも標準の @keyframes 構文を使用し、transformopacity をアニメーションする場合はコンポジタースレッドで実行されるため、スムーズな60fpsのパフォーマンスが保証されます。

スクロール駆動のフェードイン — プレビュー内でスクロールしてください

コード例

読了プログレスバー

ユーザーがページをスクロールするにつれて埋まるプログレスバーです:

.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: var(--color-primary, #2563eb);
transform-origin: left;
animation: scale-progress linear;
animation-timeline: scroll();
}

@keyframes scale-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
<div class="progress-bar" aria-hidden="true"></div>

スクロール時のフェードイン(View Timeline)

要素がビューポートに入るとフェードインします:

.reveal {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}

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

animation-range: entry 0% entry 100% は、要素がビューポートに入り始めてから完全に内側に入るまでアニメーションが再生されることを意味します。

パララックス背景

.hero {
position: relative;
height: 80vh;
overflow: hidden;
}

.hero__background {
position: absolute;
inset: -20% 0;
background-image: url("hero.jpg");
background-size: cover;
background-position: center;
animation: parallax linear;
animation-timeline: scroll();
}

@keyframes parallax {
from {
transform: translateY(-10%);
}
to {
transform: translateY(10%);
}
}

背景がスクロールよりも遅く移動することで、パララックスの奥行き効果を生み出します。完全にJavaScript不要です。

スティッキーヘッダーのスクロール時縮小

.header {
position: sticky;
top: 0;
animation: header-shrink linear both;
animation-timeline: scroll();
animation-range: 0 200px;
}

@keyframes header-shrink {
from {
padding-block: 1.5rem;
font-size: 1.5rem;
}
to {
padding-block: 0.5rem;
font-size: 1rem;
}
}

animation-range: 0 200px はアニメーションを最初の200pxのスクロールに制限するため、ヘッダーは素早く縮小してから縮小された状態を維持します。

View Timeline を使用したスタガード付きリビールアニメーション

.card {
animation: card-reveal linear both;
animation-timeline: view();
animation-range: entry 10% entry 90%;
}

@keyframes card-reveal {
from {
opacity: 0;
transform: translateY(32px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}

各カードがビューポートに入ると、他のカードとは独立してリビールアニメーションがトリガーされます。

特定コンテナの水平スクロールプログレス

.scrollable-section {
overflow-y: auto;
max-height: 60vh;
scroll-timeline-name: --section-scroll;
scroll-timeline-axis: block;
}

.section-progress {
height: 3px;
background: var(--color-primary, #2563eb);
transform-origin: left;
animation: scale-progress linear;
animation-timeline: --section-scroll;
}

@keyframes scale-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}

名前付きスクロールタイムライン(scroll-timeline-name)を使用すると、ドキュメントルートだけでなく、特定のスクロールコンテナにアニメーションをリンクできます。

View Timeline のレンジの解説

animation-range プロパティは、View Timeline の名前付きレンジを受け取ります:

/* Full visibility lifecycle */
.element {
animation-timeline: view();
}

/* entry: element enters the viewport */
.entry-anim {
animation-range: entry 0% entry 100%;
}

/* exit: element leaves the viewport */
.exit-anim {
animation-range: exit 0% exit 100%;
}

/* contain: element is fully contained in viewport */
.contain-anim {
animation-range: contain 0% contain 100%;
}

/* cover: from first pixel entering to last pixel leaving */
.cover-anim {
animation-range: cover 0% cover 100%;
}

AIがよくやるミス

  • スクロール連動アニメーションにJavaScriptを使用する:CSS animation-timeline がネイティブで処理できるユースケースに addEventListener('scroll') やIntersection Observerを使おうとします。
  • このAPIの存在を知らない:最もよくあるミスです。AIエージェントはCSSだけで処理できるプログレスバーやリビールアニメーションにJavaScriptを生成します。
  • animation-range を忘れるanimation-range がないと、View Timeline アニメーションは可視性のライフサイクル全体にまたがります。ほとんどのユースケースでは entrycontain のような特定のレンジが必要です。
  • 高コストなプロパティをアニメーションするheightmargin のようなレイアウトをトリガーするプロパティにScroll-Driven Animationsを使用します。コンポジタースレッドのパフォーマンスのために transformopacity に限定しましょう。
  • フォールバックを提供しない:Scroll-Driven Animationsはすべてのブラウザでサポートされているわけではありません(Firefoxは限定的なサポート)。アニメーションなしでもコンテンツが表示され使用可能であることを常に確認しましょう。
  • view() が適切な場面で scroll() を使用するscroll() はスクロールコンテナの位置をグローバルに追跡します。view() は個々の要素の可視性を追跡します。要素のリビールには scroll() ではなく view() が必要です。

使い分け

  • 読了プログレスバー:ドキュメントスクロールを追跡する scroll() タイムラインに使います。
  • 要素のリビールアニメーション:要素がビューポートに入る際のフェードイン、スライドアップ、スケールアニメーションをトリガーする view() タイムラインに使います。
  • パララックスエフェクト:背景レイヤーを異なる速度で移動させる scroll() タイムラインに使います。
  • ヘッダーの変形:スクロール深度に基づいてスティッキーヘッダーを縮小・リスタイリングする場合に使います。
  • 複雑なインタラクションロジックには不向き:Scroll-Driven Animationsは宣言的で、スクロール位置に応答します。「一度だけアニメーション」や「ディレイ後にトリガー」のようなロジックには、JavaScriptやIntersection Observerが必要な場合があります。
  • 常にプログレッシブエンハンスメントとして:アニメーションなしでもコンテンツが機能することを確認しましょう。必要に応じて @supports (animation-timeline: scroll()) でフィーチャーディテクションを使用できます。

参考リンク