CSSでスクロール連動を作るScroll-driven Animationsの使い方

css
記事内に広告が含まれています。

スクロール位置に応じてアニメーションを連動させたい。これまでWeb制作者にとって、こうした要望を実現するには JavaScript による実装がほぼ必須でした。scroll イベント、Intersection Observer、GSAP など、様々なアプローチがありますが、どれもコード記述の手間と保守コストがかかります。

しかし2023年のCSS Animations Level 2仕様更新により、状況が変わりました。スクロール位置をアニメーションの進行度に直結させるScroll-driven Animationsが登場し、CSSだけでスムーズなスクロール連動表現が実現可能になったのです。プログレスバー、フェードイン、Sticky Header、SVGアニメーション——これらが今、JavaScriptなしで実装できます。

とはいえ、「animation-timeline」「scroll()」「view()」「animation-range」といった新しい概念は、情報が少なく、実装方法がわかりにくいのが現状です。「仕組みは理解したけど、実装で躓いた」「期待通りに動かない」という声も多く聞きます。また、ブラウザ対応状況や、パフォーマンスへの影響についても、正確な情報が求められています。

このため、この記事では、Scroll-driven Animationsの基礎から実装、トラブルシューティングまでの情報を網羅しました。

この記事を読んでわかること

  • Scroll-driven Animationsとは何か、従来のCSS animationとどう違うのか
  • Scroll Timeline と View Timeline の違いと、使い分けのポイント
  • JavaScriptの scroll イベント、Intersection Observer、GSAPと、それぞれどう使い分けるべきか
  • animation-timeline、animation-range、scroll()、view() の具体的な書き方
  • プログレスバー、フェードイン、Sticky Header など、5つの実装パターン(コード付き)
  • 期待通りに動かない場合の原因特定とデバッグ方法
  • transform と opacity を活用したパフォーマンス最適化テクニック
  • prefers-reduced-motion への対応とアクセシビリティの考慮
  • 実務で迷いやすい12個のFAQ
  • 2026年現在のブラウザ対応状況と、今後の実装戦略

Scroll-driven animationsとは?仕組み・特徴・従来手法との違い

scroll-driven-animations

Scroll-driven animationsの意味と従来のCSSアニメーションとの違い

Scroll-driven Animationsは、スクロール位置を基準にCSSアニメーションの進行度をコントロールする仕組みです。@keyframes で定義したアニメーションそのものは従来と同じですが、進行を決める軸が「時間」から「スクロール」に変わる点が最大の違いになります。

通常のCSS Animationは、animation-duration で指定した時間が経過するとアニメーションが自動的に進みます。ページを開いた瞬間から時計が動き出すようなイメージです。一方、Scroll-driven Animationsでは animation-timeline プロパティにスクロールベースのタイムラインを指定することで、時間の代わりにスクロール量がアニメーションの進行度を決めるようになります。ユーザーがスクロールした分だけアニメーションが進み、止まればアニメーションも止まる。上にスクロールし直せば、アニメーションも逆再生されます。

この仕組みは、CSS Animations Level 2の仕様として2023年6月に animation-timeline が策定されたことで実現しました。仕様上は「CSS scroll-driven animations」というモジュールにまとまっており、Web Animations APIとCSS Animationsの両方から利用できるよう設計されています。

ここで一つ、混同しやすい点を整理しておきます。Scroll-driven Animationsは「スクロール量に応じてアニメーションが連続的に進む・戻る」タイプの動きを指します。これに対して、特定のスクロール位置を通過した瞬間に通常の時間ベースアニメーションを一度だけ再生する仕組みは「scroll-triggered animation」と呼ばれ、animation-trigger という別のプロパティで実現します。こちらはScroll-driven Animationsより新しい仕様で、CSSWG(CSS Working Group)にて議論・仕様化が進められている標準化途上の試験的機能(ドラフト段階・Chrome等で実装検証中)です。本記事で扱うのは前者、スクロール量そのものをアニメーションの進行度として使う「driven(連動)」の仕組みです。

Scroll TimelineとView Timelineの仕組み

Scroll-driven Animationsには、性質の異なる2種類のタイムラインがあります。それぞれ「何を時間軸として扱うか」が異なるため、混同すると意図した動きになりません。

Scroll Timelineは、スクロールコンテナそのもののスクロール位置を時間軸にします。スクロール可能な要素(多くの場合はページ全体、あるいは overflow: scroll を指定した要素)が、スクロール範囲の先頭(0%)から末尾(100%)までどれだけ進んだかによってアニメーションの進行度が決まります。ページ全体の読了率に応じて進むプログレスバーのように、「今どれだけスクロールしたか」を表現したい場合に向いています。

View Timelineは、対象要素そのものがスクロールコンテナのビューポート(表示領域)に入ってから出ていくまでの過程を時間軸にします。スクロール位置ではなく、「その要素が画面のどこに、どれだけ見えているか」が基準になる点がScroll Timelineとの決定的な違いです。要素がビューポート下端から入り始めた瞬間を0%、完全に上端を通過して見えなくなった瞬間を100%として進行度が計算されます。フェードインやスケールアップなど、要素単位の出現アニメーションを作る場合はこちらを使います。

両者を「何が時間軸になるか」という観点で整理すると、次のようになります。

タイムライン時間軸になるもの向いている用途
Scroll Timelineスクロールコンテナ全体のスクロール位置(0%〜100%)読了率プログレスバー、ページ全体に連動する演出
View Timeline対象要素がビューポートを通過する過程(入り始め〜出終わり)個別要素のフェードイン、スケール、要素ごとの出現演出

実装上は、scroll-timeline プロパティ(またはショートハンドの animation-timeline: scroll())でScroll Timelineを、view-timeline プロパティ(または animation-timeline: view())でView Timelineを指定します。名前付きのタイムラインを使う場合は scroll-timeline-nameview-timeline-name、スクロール方向を指定する view-timeline-axis、要素とビューポート端との余白を調整する view-timeline-inset、名前付きタイムラインの参照範囲を制御する timeline-scope といったプロパティも用意されています。これらの具体的な書き方は次の章で扱います。

なお、Scroll-driven Animationsでは animation-duration の秒数指定に意味がなくなります。進行度を決めるのはスクロール量であって時間ではないため、animation-durationauto(もしくは省略)を指定するのが基本です。ここを従来の感覚のまま秒数で指定してしまうと、意図と異なる挙動になることがあるので注意してください。

JavaScriptのscrollイベント・Intersection Observer・GSAPとの違い

スクロールに連動した表現は、これまでJavaScriptで実装するのが一般的でした。Scroll-driven Animationsが登場した今も、JavaScriptによる手法が不要になったわけではなく、それぞれ得意なことが異なります。

手法主な用途特徴
Scroll-driven Animationsスクロール位置とCSSアニメーションの連動CSSだけで完結する。ブラウザの合成(コンポジター)スレッドでアニメーションが処理されるため、メインスレッドの負荷に影響されにくい
scrollイベントスクロール位置を使った複雑な処理JavaScriptによる自由度が高く、CSSのアニメーションでは表現しきれない条件分岐や外部連携ができる
Intersection Observer要素の表示・非表示の検知要素が画面に入った・出たという「状態の変化」を検知するのが得意。連続的な進行度の取得には向かない
GSAP(ScrollTrigger)高度なアニメーション制御複雑なイージング、タイムラインの連結、ピン留め演出など、演出の自由度と表現力が高い

scroll イベントは、スクロール位置を使いながらDOMの複雑な操作やAPI呼び出しなど、CSSでは実現できない処理を組み込みたい場合に向いています。ただし、スクロールのたびにイベントハンドラがメインスレッドで実行されるため、処理が重くなるとスクロールのカクつきにつながりやすい性質があります。

Intersection Observerは、要素が「見えているかどうか」を非同期に検知する仕組みで、遅延読み込みや、一度だけ発火させたいフェードインの起点を検知する用途に適しています。ただし、これは「入った・出た」という離散的なイベントの検知であり、View Timelineのようにスクロールに合わせて連続的に進行度を追従させる用途には向いていません。

GSAPは、Scroll-driven Animationsやほかの手法と比べて表現の自由度が段違いに高いライブラリです。複数要素を連携させたタイムライン制御や、独自のイージング関数、ピン留めしながらの複雑な演出など、CSSの animation-timeline だけでは組みにくい表現を実装したい場合には依然として有力な選択肢になります。

一方でScroll-driven Animationsは、「スクロール位置や要素の表示位置に応じて、決められたCSSプロパティを滑らかに変化させる」というシンプルな用途に強みがあります。ライブラリの読み込みが不要で、CSSの記述だけで完結するため、実装コストと保守コストを抑えたいケースに向いています。次の章では、この仕組みを実際に使うための基本構文を見ていきます。

Scroll-driven Animationsの基本構文と主要プロパティの使い方

Scroll-driven Animationsの実装は、普段書いている @keyframes に、スクロールをタイムラインとして紐づけるプロパティを追加するだけです。ここでは、実装の核になる3つのプロパティを順番に見ていきます。

animation-timelineプロパティとscroll()・view()関数の使い分け

animation-timeline は、アニメーションの進行度を何によって決めるかを指定するプロパティです。通常のCSS Animationでは指定を省略した状態(初期値は auto)になっており、この場合は経過時間がそのままアニメーションの進行度になります。ここに scroll()view() を指定すると、進行度の基準がスクロールに切り替わります。

まずは最小構成のコードで挙動を確認してみます。

<div class="scroll-box">
  <div class="progress-fill"></div>
</div>
<div class="content">
  <p>ここをスクロールしてください。</p>
  <p style="margin-top: 150vh;">スクロールに応じて上のバーが伸びます。</p>
</div>
.scroll-box {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 6px;
  background: #eee;
}

.progress-fill {
  height: 100%;
  background: #3b82f6;
  transform-origin: left;
  /* 初期状態を0%に固定 */
  transform: scaleX(0);
}

/* 対応ブラウザのみアニメーションを有効化 */
@supports (animation-timeline: scroll()) {
  .progress-fill {
    animation: fill-bar linear;
    animation-timeline: scroll(root block);
    animation-fill-mode: both;
  }
}

@keyframes fill-bar {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

実際の表示

See the Pen Scroll-driven-Animations-base by watashi-xyz (@watashi-xyz) on CodePen.

animation-timeline: scroll(root block) の部分が、Scroll Timelineを使うための指定です。scroll() 関数は、どのスクロールコンテナを基準にするかを表す scroller キーワード(nearest / root / self)と、どのスクロール方向を基準にするかを表す axis キーワード(block / inline / x / y)を受け取ります。root はビューポート全体のスクロール、nearest は要素にとって最も近い祖先のスクロールコンテナ(初期値)、self は要素自身がスクロールコンテナである場合に使います。引数を省略した scroll() だけの記述も可能で、その場合は nearest block が使われます。

一方、要素単位の出現アニメーションを作るときに使うのが view() です。

.reveal-item {
  animation: fade-up linear both;
  animation-timeline: view(block);
}

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

view() 関数は、対象要素自身がスクロールコンテナのビューポートをどのように通過するかを基準にします。引数には axis キーワード(block / inline / x / y)と、要素がビューポート端からどれだけ離れた位置で開始・終了とみなすかを調整する inset 値を指定できます。view(block 20%) のように書くと、ビューポートの上下20%分を「見えていない」領域として扱うといった調整が可能です。

scroll()view() は似ているようで、基準にしているものがまったく異なります。scroll() はスクロールコンテナの移動量、view() は対象要素とビューポートの位置関係です。「ページ全体の読了率を表現したい」なら scroll()、「この要素が画面に入ってきたら動かしたい」なら view() という判断基準で使い分けると迷いにくくなります。

animation-rangeでアニメーションの開始・終了位置を細かく制御する方法

animation-timeline を指定しただけでは、タイムライン全体(0%〜100%)にアニメーションが割り当てられます。アニメーションを「いつ始めて、いつ100%まで進めるか」を調整するのが animation-range です。ショートハンドで、animation-range-startanimation-range-end をまとめて指定できます。

Scroll Timelineの場合、animation-range にはタイムライン全体に対するパーセンテージを指定します。たとえば animation-range: 20% 80% と書くと、スクロール進行度が20%に達した時点でアニメーションが0%として始まり、80%に達した時点で100%まで進み切る、という制御になります。

View Timelineの場合はもう少し複雑です。対象要素がビューポートを通過する過程は、次のような区間に分けて考えられます。

  • entry:要素がビューポートに入り始めてから、完全に入り終わるまで
  • contain:要素がビューポート内に収まっている間(要素がビューポートより大きい場合は範囲が変わります)
  • exit:要素がビューポートから出始めてから、完全に出終わるまで
  • cover:要素が最初に見え始めてから、完全に見えなくなるまでの全区間

これらの区間名にパーセンテージを組み合わせて範囲を指定します。たとえば次のように書くと、要素が画面に入ってくる過程(entry区間)だけでアニメーションを完結させられます。

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

文章だけだとイメージしづらいので、スクロールの流れに沿って整理します。要素がビューポート下端からちょうど入り始めた瞬間が entry 0%、要素全体が完全にビューポート内へ入り切った瞬間が entry 100% です。この2点の間だけをアニメーションの進行区間として使うため、要素が画面に入りきった後は途中の状態のまま止まり、画面から出ていく際には動きません。逆に、要素が出ていく動きも付けたい場合は animation-range: entry 0% exit 100% のように範囲を広げます。

animation-range を省略した場合の初期値は normal normal で、これはタイムラインの0%から100%まで、つまりScroll Timelineなら全スクロール範囲、View Timelineなら cover区間(見え始めから見えなくなるまでの全体)に相当します。

通常のCSS animation(duration等)との関係と設定時の注意点

Scroll-driven Animationsも @keyframes を使う以上、通常のCSS Animationのプロパティと無関係ではありません。ただし、いくつか従来と異なる考え方をする部分があります。

まず animation-duration です。時間の経過ではなくスクロールが進行度を決めるため、秒数を指定しても意味を持ちません。基本的には初期値の auto のままにしておきます。animation ショートハンドで秒数を書いてしまっている場合も、animation-timeline を指定すればスクロールが優先されるため大きな実害はありませんが、コードを読む人が混乱しないよう auto にしておくのが無難です。

次に注意したいのが、プロパティを書く順番です。animation-timelineanimation ショートハンドに含まれるプロパティではありません。そのため、次のように animation ショートハンドの後に animation-timeline を書く必要があります。

/* 正しい書き方 */
.reveal-item {
  animation: fade-up linear both;
  animation-timeline: view(block);
}

/* 動かない例:ショートハンドが後にあると各プロパティが初期値に戻ってしまう */
.reveal-item {
  animation-timeline: view(block);
  animation: fade-up linear both;
}

animation ショートハンドは、指定されなかったサブプロパティを初期値でリセットする性質があります。animation-timeline を先に書いてしまうと、後から書いた animation ショートハンドによって animation-timeline の指定が上書きされ、タイムラインの指定自体が失われてしまいます。この記述順序の間違いは、Scroll-driven Animationsが「動かない」と感じる原因の中でも特によく見られるものです。

もう一つ重要なのが animation-fill-mode です。スクロール位置が範囲外にあるとき(View Timelineでスクロール前や、要素が画面外にあるときなど)、animation-fill-mode が初期値の none のままだと、アニメーションの開始前の見た目に戻ってしまいます。from の状態を保持したまま範囲外の値も含めて反映させたい場合は、both(もしくは用途に応じて forwards / backwards)を指定しておく必要があります。上記のコード例で animation: fade-up linear both; のように both を含めているのはこのためです。

animation-iteration-countanimation-direction については、多くのScroll-driven Animationsの実装では初期値のままで問題ありません。スクロールを戻せばアニメーションも自動的に逆再生されるため、繰り返し回数や再生方向を明示的に操作する必要があるケースは限られます。次の章では、これらのプロパティを組み合わせた具体的な実装例を見ていきます。

Scroll-driven Animationsを利用したスクロールアニメーション実装例

ここからは、実装パターンごとに具体的なコード例を見ていきます。各サンプルは実際にブラウザへ貼り付けて動作確認できるものばかりです。

ページ上部に配置する読込進捗「スクロールプログレスバー」

何を作るのか

ページ上部に固定表示されるプログレスバーで、ユーザーがページをどこまでスクロールしたかを視覚化します。YouTubeやはてなブックマークなど、大手サイトでも採用されている古典的なUIパターンです。

どのような仕組みで動くのか

Scroll Timelineの代表的な使い方がこれです。ページ全体のスクロール位置(0%〜100%)を時間軸として扱い、その進行度に応じてプログレスバーの幅を0%から100%へ伸ばします。animation-timeline: scroll(root block) で「ビューポート全体のスクロール」を基準にし、transform: scaleX() で幅を制御するため、高速で描画負荷も少ないアニメーションになります。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>スクロールプログレスバー</title>
</head>
<body>

<div class="progress-bar">
  <div class="progress-fill"></div>
</div>

<main>
  <h1>スクロール連動型プログレスバー</h1>
  <p>このページをスクロールすると、上部のバーが伸びます。</p>

  <section style="margin-top: 100vh;">
    <h2>セクション1</h2>
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
  </section>

  <section style="margin-top: 100vh;">
    <h2>セクション2</h2>
    <p>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
  </section>

  <section style="margin-top: 100vh;">
    <h2>セクション3</h2>
    <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco.</p>
  </section>
</main>

</body>
</html>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 10px;
  background: #e5e7eb;
  z-index: 1000;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #3b82f6, #8b5cf6);
  transform-origin: left;
  /* 非対応ブラウザでの初期状態(幅0%) */
  transform: scaleX(0);
}

/* Scroll-driven Animations に対応しているブラウザのみアニメーションを実行 */
@supports (animation-timeline: scroll()) {
  .progress-fill {
    animation: progress linear;
    animation-timeline: scroll(root block);
    animation-fill-mode: both;
  }
}

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

main {
  padding: 2rem;
  max-width: 800px;
  margin: 0 auto;
}

h1, h2 {
  margin-top: 2rem;
  margin-bottom: 1rem;
}

p {
  line-height: 1.6;
  color: #4b5563;
}

実際の表示

See the Pen Scroll-driven-Animation-progressbar by watashi-xyz (@watashi-xyz) on CodePen.

コード解説

  • animation-timeline: scroll(root block) :ビューポート全体のブロック方向(垂直)スクロールを基準にアニメーションを進行させます
  • transform-origin: leftscaleX() を左端から右方向へ拡大させるため、左端を基点に指定しています
  • animation-fill-mode: both :ページを上にスクロールして0%に戻っても、プログレスバーが消えないよう、常に最新の状態を反映させます
  • transform: scaleX() を使うことで、幅を直接変更する場合より描画負荷が低く、スクロール中も高速に追従します

実装時の注意点

  • animation-duration は指定しません。スクロールが時間軸になっているため秒数は不要です。もし指定してしまっても動作しますが、混乱を避けるため auto にするか省略してください
  • プログレスバーを含む要素に overflow スタイルを指定していないことを確認してください。スクロールコンテナになってしまうと scroll(root) の基準が変わる可能性があります
  • 背景色を濃くしすぎるとテキストが読みにくくなるため、高さは4〜6px程度に抑えるのが一般的です

どのようなUIで活用できるのか

  • ブログや記事ページの読了率表示
  • ドキュメントサイトのスクロール位置表示
  • オンボーディングフローにおける進捗状況表示
  • 長い形式のコンテンツにおけるユーザーへの心理的なフィードバック

ビューポートに入ると要素が出現する「フェードイン&拡大・移動」

何を作るのか

スクロールして要素がビューポートに入ってくるにつれて、透明度が上がり、位置がシフトし、サイズが大きくなるアニメーション。Web制作で最頻出のパターンの一つです。

どのような仕組みで動くのか

View Timelineを使い、対象要素がビューポートに入ってから出ていくまでの過程をアニメーションの進行度に割り当てます。ここでは animation-range: entry 0% entry 100% として「入ってくる過程だけでアニメーションを完了させる」設定にしているため、要素が完全に画面内へ入ると同時にアニメーションが100%まで進み、その後はそこで止まります。スクロール位置をビューポート内での見え方の相対値で制御する view-timeline-inset を使うことで、タイミングを調整することもできます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>要素フェードイン</title>
</head>
<body>

<div style="height: 100vh; display: flex; align-items: center; justify-content: center;">
  <h1>下にスクロールしてください</h1>
</div>

<article>
  <div class="reveal-card">
    <h2>カード1</h2>
    <p>スクロールして画面に入ると、このカードがフェードインして拡大します。</p>
  </div>

  <div class="reveal-card">
    <h2>カード2</h2>
    <p>各カードは独立したアニメーションで、個別にトリガーされます。</p>
  </div>

  <div class="reveal-card">
    <h2>カード3</h2>
    <p>View Timelineを使っているため、JavaScript不要で実装できます。</p>
  </div>

  <div class="reveal-card">
    <h2>カード4</h2>
    <p>最後のカードまでスクロールしてすべてのアニメーションをご覧ください。</p>
  </div>
</article>

</body>
</html>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

article {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
}

.reveal-card {
  background: white;
  border-radius: 8px;
  padding: 2rem;
  margin-bottom: 2rem;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);

  animation: reveal-up linear both;
  animation-timeline: view(block);
  animation-range: entry 0% entry 100%;
}

.reveal-card h2 {
  margin-bottom: 0.5rem;
  color: #1f2937;
}

.reveal-card p {
  color: #6b7280;
  line-height: 1.6;
}

@keyframes reveal-up {
  from {
    opacity: 0;
    transform: translateY(50px) scale(0.5);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

実際のコード

See the Pen Scroll-driven-Animation-fadein by watashi-xyz (@watashi-xyz) on CodePen.

コード解説

  • animation-timeline: view(block) :対象要素がビューポートの上下方向をどのように通過するかを基準にします
  • animation-range: entry 0% entry 100% :要素が画面に入ってくる区間(entry)の、0%から100%までだけをアニメーション区間に指定。入り切った後は途中の状態で止まります
  • transform: translateY(40px) scale(0.95) で、下から上へ40px移動、初期状態は95%のサイズにしています。これにより、滑らかな登場感が生まれます
  • opacity: 0 から opacity: 1 へのフェードを組み合わせることで、より柔らかい出現演出になります

実装時の注意点

  • animation-rangeentry 0% entry 100% に指定していない場合、要素がビューポートから出ていく際(exit区間)もアニメーションが逆再生されます。必要に応じて animation-range: cover 0% cover 100% のように全体の視野に合わせた範囲に変更してください
  • view-timeline-inset を使ってタイミングを調整する場合、ビューポート高さに対する相対値で指定します。例えば .reveal-card { view-timeline-inset: auto -50px; } とすると、要素がビューポート下部から50px手前で開始点と判定されます
  • 複数の要素に同じアニメーションを指定する場合、各要素が独立したView Timelineを持つため、それぞれで個別にトリガーされます

どのようなUIで活用できるのか

  • サービス紹介ページのカードセクション
  • ブログやニュースサイトの記事リスト
  • eコマースの商品リスト表示
  • ポートフォリオサイトの作品紹介
  • “About Us”セクションのチームメンバーカード

スクロールに応じて変化を追従させる「Sticky Heading」

何を作るのか

スクロール位置に応じて、見出しのサイズ、背景色、テキスト色などが段階的に変化するヘッダー。サイトレベルでのブランディングや、長いページの進捗状況を視覚的に示すUIとして機能します。

どのような仕組みで動くのか

position: sticky でヘッダーを固定しておき、その上に Scroll-driven Animations で見た目の変化を重ねます。ヘッダー自体の位置は sticky が担当し、その状態(色、透明度、サイズなど)の変化が animation-timeline で制御されます。見出しが大きい状態から小さい状態へ、背景が透明から不透明へと徐々に変わっていくため、読者はスクロール距離を視覚的に把握できます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sticky Heading</title>
</head>
<body>

<header class="sticky-header">
  <h1 class="header-title">ブログタイトル</h1>
</header>

<main>
  <section style="margin-top: 80vh;">
    <h2>最初のセクション</h2>
    <p style="margin-bottom: 60vh;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
  </section>

  <section style="margin-top: 40vh;">
    <h2>次のセクション</h2>
    <p style="margin-bottom: 60vh;">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
  </section>

  <section style="margin-top: 40vh;">
    <h2>さらに次のセクション</h2>
    <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
  </section>
</main>

</body>
</html>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.sticky-header {
  position: sticky;
  top: 0;
  z-index: 100;
  height: 80px;
  display: flex;
  align-items: center;
  padding: 0 2rem;
}

.header-title {
  font-size: clamp(1.5rem, 3vw, 2.5rem);
}

/* Scroll-driven Animations に対応しているブラウザのみアニメーションを有効化 */
@supports (animation-timeline: scroll()) {
  .sticky-header {
    animation: header-shrink linear;
    animation-timeline: scroll(root block);
    animation-range: 0px 300px;
    animation-fill-mode: both;
  }

  .header-title {
    animation: title-shrink linear;
    animation-timeline: scroll(root block);
    animation-range: 0px 300px;
    animation-fill-mode: both;
  }
}

@keyframes header-shrink {
  from {
    background: rgba(255, 255, 255, 0);
    box-shadow: none;
    border-bottom: 1px solid rgba(0, 0, 0, 0);
  }
  to {
    background: rgba(255, 255, 255, 0.95);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  }
}

@keyframes title-shrink {
  from {
    font-size: 2rem;
    color: #1f2937;
  }
  to {
    font-size: 1.25rem;
    color: #6b7280;
  }
}

main {
  max-width: 900px;
  margin: 0 auto;
  padding: 2rem;
}

section {
  margin-bottom: 4rem;
}

h2 {
  margin-bottom: 1rem;
  font-size: 1.75rem;
  color: #1f2937;
}

p {
  line-height: 1.8;
  color: #4b5563;
}

実際の表示

See the Pen Scroll-driven-Animations sticky heading by watashi-xyz (@watashi-xyz) on CodePen.

コード解説

  • animation-range: 0px 300px :スクロール開始位置(0px)から300pxまでの間でアニメーションを進行させます。従来のパーセンテージ指定ではなく、ピクセル単位で明示的に範囲を指定しています
  • animation-timeline: scroll(root block) :ページ全体のスクロール位置を基準にしています。position: sticky と組み合わせることで、ヘッダーは固定位置を保ちながら、アニメーション だけが進みます
  • .header-title.sticky-header に別々のアニメーションを指定しており、背景色とテキスト色、フォントサイズが独立して変化します
  • box-shadow の透明度を rgba(0, 0, 0, 0) から rgba(0, 0, 0, 0.1) へ変更することで、ヘッダーが画面に「くっついた」感じを演出しています

実装時の注意点

  • animation-range: 0px 300px のようにピクセル単位を使う場合、ビューポートの高さやコンテンツの分量によって調整が必要です。300pxで足りない場合は値を増やしてください
  • position: stickyanimation-timeline: scroll() は独立して動作します。sticky がヘッダー位置を固定し、アニメーションは見た目(背景色、サイズなど)だけを制御する分担が重要です
  • clamp() 関数を使ったレスポンシブなフォントサイズ設定と組み合わせる場合、モバイルでもテキストが読みやすいサイズを下限として指定してください

どのようなUIで活用できるのか

  • ブログやオンラインマガジンのヘッダー
  • SaaS製品のランディングページ
  • オンボーディングフローのステップ表示
  • ドキュメントサイトのナビゲーションバー
  • 商品詳細ページのタイトルセクション

スクロール量に合わせてSVGの線が伸びるアニメーション

何を作るのか

SVGで描いた線や道が、スクロール位置に応じて少しずつ描かれていくような表現。ビジュアル的に印象的で、かつスクロール位置を直感的に示すUIになります。

どのような仕組みで動くのか

SVGの線を描くときに使う stroke-dasharraystroke-dashoffset を活用します。stroke-dasharray で線を「実線」と「空白」の繰り返しパターンで定義し、stroke-dashoffset でそのパターンをずらすことで、線が少しずつ現れているように見えます。Scroll-driven Animationsでこのオフセット値を連続的に変化させれば、スクロールに追従して線が伸びていく表現が実現できます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SVGスクロールライン</title>
</head>
<body>

<div class="svg-container">
  <svg viewBox="0 0 100 600" width="100" height="600" xmlns="http://www.w3.org/2000/svg">
    <path
      class="drawing-path"
      d="M 50 0 Q 30 50, 50 100 T 50 300 Q 70 350, 50 400 T 50 600"
      fill="none"
      stroke="#3b82f6"
      stroke-width="3"
      stroke-linecap="round"
    />
  </svg>
</div>

<div class="content" style="margin-top: 100vh;">
  <h1>SVGを使ったスクロール表現</h1>
  <p>上のSVGの線がスクロール位置に応じて伸びていきます。</p>
  <p style="margin-top: 60vh;">このような視覚効果は、タイムラインビジュアライゼーションやロードマップUIに活用できます。</p>
</div>

</body>
</html>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.svg-container {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 120px;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 10;
}

svg {
  filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.3));
}

.drawing-path {
  stroke-dasharray: 1000;
  /* 非対応ブラウザ用のフォールバック(線を表示させておく) */
  stroke-dashoffset: 0;
}

/* Scroll-driven Animations に対応しているブラウザのみアニメーションを有効化 */
@supports (animation-timeline: scroll()) {
  .drawing-path {
    stroke-dashoffset: 1000;
    animation: draw-line linear;
    animation-timeline: scroll(root block);
    animation-fill-mode: both;
  }
}

@keyframes draw-line {
  from {
    stroke-dashoffset: 1000;
  }
  to {
    stroke-dashoffset: 0;
  }
}

.content {
  max-width: 800px;
  margin-left: auto;
  margin-right: auto;
  padding: 2rem;
  background: white;
  position: relative;
  z-index: 20;
}

h1 {
  font-size: 2rem;
  margin-bottom: 1rem;
  color: #1f2937;
}

p {
  line-height: 1.8;
  color: #4b5563;
  margin-bottom: 1rem;
}

実際の表示

See the Pen Scroll-driven-Animations svg line by watashi-xyz (@watashi-xyz) on CodePen.

コード解説

  • stroke-dasharray: 1000 :線の全体の長さを1000単位に設定。実際のSVG線の長さに応じて、適切な値に調整が必要です
  • stroke-dashoffset: 1000 :初期状態では、オフセットが配列の全長と同じため、線が全く見えない状態です
  • @keyframes draw-linestroke-dashoffset を1000から0へ変化させることで、線が徐々に描かれます
  • animation-timeline: scroll(root block) でスクロール位置に追従させます

実装時の注意点

  • stroke-dasharray の値はSVG線の実際の長さに応じて調整が必要です。値が小さすぎるとスクロール途中に線の描画が完了し、大きすぎるとアニメーションが緩い動きになります。デベロッパーツールで試行錯誤して最適な値を見つけてください
  • 複雑なパスを使う場合、複数の <path> 要素に分けて、それぞれ異なる animation-range を指定することで、段階的な描画も可能です
  • 曲線が多い場合、stroke-linecap: roundstroke-linejoin: round を指定すると、より滑らかな見た目になります
  • このサンプルではスクロール全体のタイムラインを使っていますが、特定の要素のView Timelineに紐づけることも可能です

どのようなUIで活用できるのか

  • タイムラインビジュアライゼーション(企業の成長過程、プロジェクト工程など)
  • ロードマップやマイルストーン表示
  • ページのセクション遷移を表す視覚的なガイド
  • インタラクティブなストーリーテリング
  • サービス利用フロー図

CSS Scroll Snapと組み合わせたリッチな「カルーセルUI」

何を作るのか

水平スクロール可能なカルーセルで、スクロール位置に応じてアイテムが自動的にスナップされ、同時に現在表示されているアイテムの見た目が変化するUI。複数の機能を組み合わせたリッチな体験が実現します。

どのような仕組みで動くのか

CSS Scroll SnapとScroll-driven Animationsは、役割が明確に分かれています。CSS Scroll Snapは「スクロール位置を特定の地点へ自動的にスナップさせる」ための仕組みで、Scroll-driven Animationsは「スクロール位置に応じてアニメーションを進行させる」ための仕組みです。両者を組み合わせると、スナップが効いた滑らかなスクロール位置の遷移と、それに同期した視覚的フィードバック(アイテムの拡大、背景色の変化など)が実現できます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Scroll Snapカルーセル</title>
</head>
<body>

<div class="carousel-container">
  <div class="carousel-track">
    <div class="carousel-item">
      <div class="item-content">
        <h2>アイテム1</h2>
        <p>最初のカルーセルアイテム</p>
      </div>
    </div>
    <div class="carousel-item">
      <div class="item-content">
        <h2>アイテム2</h2>
        <p>2番目のカルーセルアイテム</p>
      </div>
    </div>
    <div class="carousel-item">
      <div class="item-content">
        <h2>アイテム3</h2>
        <p>3番目のカルーセルアイテム</p>
      </div>
    </div>
    <div class="carousel-item">
      <div class="item-content">
        <h2>アイテム4</h2>
        <p>4番目のカルーセルアイテム</p>
      </div>
    </div>
  </div>
  <div class="carousel-indicator"></div>
</div>

<div class="scroll-instruction">
  ← 左右にスクロールしてください →
</div>

</body>
</html>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  background: #f3f4f6;
}

.carousel-container {
  width: 100vw;
  height: 100vh;
  overflow-x: scroll;
  overflow-y: hidden;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
}

.carousel-track {
  display: flex;
  height: 100%;
}

.carousel-item {
  flex: 0 0 100vw;
  scroll-snap-align: center;
  scroll-snap-stop: always;
  display: flex;
  align-items: center;
  justify-content: center;
}

.carousel-item:nth-child(1) {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

.carousel-item:nth-child(2) {
  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}

.carousel-item:nth-child(3) {
  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}

.carousel-item:nth-child(4) {
  background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}

.item-content {
  color: white;
  text-align: center;
}

.item-content h2 {
  font-size: 3rem;
  margin-bottom: 1rem;
}

.item-content p {
  font-size: 1.25rem;
  opacity: 0.9;
}

.carousel-indicator {
  position: fixed;
  bottom: 2rem;
  left: 50%;
  transform: translateX(-50%);
  width: 120px;
  height: 4px;
  background: rgba(255, 255, 255, 0.3);
  border-radius: 2px;
  z-index: 100;
}

.scroll-instruction {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: 1.5rem;
  color: rgba(255, 255, 255, 0.6);
  text-align: center;
  pointer-events: none;
  z-index: 5;
}

/* Scroll-driven Animations 対応ブラウザ向けの処理 */
@supports (animation-timeline: view()) {
  /* 各アイテムの表示・非表示エリア通過時に連動 */
  .carousel-item {
    animation: item-scale linear both;
    animation-timeline: view(inline);
  }

  /* テキストの個別の通過に連動 */
  .item-content {
    animation: content-reveal linear both;
    animation-timeline: view(inline);
  }

  /* インジケーターのみ全体のスクロール量に連動 */
  .carousel-indicator {
    animation: indicator-fill linear both;
    animation-timeline: scroll(nearest inline);
  }
}

/* 進入(0%) ➔ 中央到達(50%) ➔ 退出(100%) のキーフレーム */
@keyframes item-scale {
  0% {
    transform: scale(0.8);
    filter: blur(8px);
    opacity: 0.5;
  }
  50% {
    transform: scale(1);
    filter: blur(0);
    opacity: 1;
  }
  100% {
    transform: scale(0.8);
    filter: blur(8px);
    opacity: 0.5;
  }
}

@keyframes content-reveal {
  0% {
    opacity: 0;
    transform: translateY(30px);
  }
  50% {
    opacity: 1;
    transform: translateY(0);
  }
  100% {
    opacity: 0;
    transform: translateY(-30px);
  }
}

@keyframes indicator-fill {
  from {
    transform: translateX(-50%) scaleX(0);
  }
  to {
    transform: translateX(-50%) scaleX(1);
  }
}

実際の表示

See the Pen Scroll-driven-Animations Carousel by watashi-xyz (@watashi-xyz) on CodePen.

コード解説

  • scroll-snap-type: x mandatory :水平方向のスクロールスナップを有効にし、スクロール後に最も近い snap point へ強制的にスナップさせます(mandatory
  • scroll-snap-align: center :各カルーセルアイテムがビューポート中央に来るようにスナップされます
  • scroll-snap-stop: always :スクロール中のスキップを防ぎ、必ずこのアイテムで一度停止するよう強制します
  • animation-timeline: scroll(nearest inline) :各アイテムに対して最も近いスクロールコンテナのインライン方向(水平)スクロールを基準にアニメーションを進行させます
  • .carousel-indicator で全体のスクロール進度を表すバーを animation-timeline: scroll(root inline) で制御し、ページ全体のスクロール位置を可視化しています

実装時の注意点

  • CSS Scroll Snapは「どこでスナップするか」を制御するもので、Scroll-driven Animationsは「スクロール位置に応じた視覚的変化」を制御するものです。両者は独立しており、順序を逆にしても両立できます
  • scroll-behavior: smooth を使うとスクロール時の移動がスムーズになりますが、ブラウザによってはパフォーマンスに影響が出ることがあるため、必要に応じて確認してください
  • モバイル環境では、水平スクロールのUIは操作性が低下する傾向があります。タッチ友好的な設計を別途検討するか、十分な操作ガイダンスを提供してください
  • 各アイテムの animation-range を個別に指定することで、アニメーション開始のタイミングをより細かく調整することも可能です

どのようなUIで活用できるのか

  • 商品ギャラリーやポートフォリオ
  • ブランド紹介サイトのスライドショー
  • オンボーディングフロー
  • ニュースやコンテンツマガジンの記事ナビゲーション
  • 画像やビデオのキューレーション表示

動かない時の原因とデバッグ・パフォーマンス最適化手法

Scroll-driven Animationsがうまく機能しないとき、原因は限定的です。このセクションでは、動かない場合の原因を特定する方法と、パフォーマンスを最適化するための工夫を紹介します。

期待通りに動かない時に確認すべきCSSとChrome DevToolsでのデバッグ

Scroll-driven Animationsが機能しない場合、以下のチェックリストを順番に確認することで、原因をかなりの確度で特定できます。

チェック1:ブラウザが機能に対応しているか

まず確認すべきはブラウザ対応状況です。2026年現在、Chrome 115以降、Safari 18以降、Edge 115、Opera 101で完全にサポートされています。Firefoxについては機能フラグ(設定)による検証段階であり、順次標準対応が進められています。Internet Explorerはもちろん、古いバージョンのブラウザでは動きません。

開発環境でテストしているなら、更新を確認してください。ユーザー環境の互換性が必要な場合は、@supports (animation-timeline: view()) で機能検出を行い、非対応ブラウザ向けのフォールバックを用意します。

@supports (animation-timeline: view()) {
  .reveal-item {
    animation: fade-up linear both;
    animation-timeline: view();
  }
}

@supports not (animation-timeline: view()) {
  .reveal-item {
    opacity: 1;
    transform: none;
  }
}

チェック2:animation-timelineが animation ショートハンドの後に記述されているか

これは非常に多い誤りです。animation ショートハンドにはサブプロパティが複数含まれ、指定されなかったものは初期値でリセットされます。animation-timelineanimation ショートハンドには含まれないため、ショートハンドより後に書く必要があります。

/* 正しい順序 */
.element {
  animation: fade-in linear both;
  animation-timeline: view();
}

/* 誤った順序:animation-timelineが無視される */
.element {
  animation-timeline: view();
  animation: fade-in linear both;
}

後者だと、animation ショートハンドによってすべての animation-* プロパティが初期状態にリセットされ、animation-timeline の指定は失われます。

チェック3:@keyframesとanimation-nameが対応しているか

単純だが見落としやすい誤りです。@keyframes の名前と、アニメーション指定時の animation-name が一致しているか確認してください。

/* 誤り:keyframes の名前と animation-name が異なる */
@keyframes fade-in-animation {
  from { opacity: 0; }
  to { opacity: 1; }
}

.element {
  animation: fade-in linear; /* fade-in-animation ではなく fade-in を指定している */
  animation-timeline: view();
}

チェック4:タイムラインの指定対象が正しいか

scroll() を使う場合、指定した scroller(root / nearest / self)が期待通りのスクロールコンテナを指しているか確認してください。特に scroll(root) で指定した場合、ウィンドウ全体のスクロールを基準にしていますが、要素が入れ子になった overflow: scroll のコンテナにあると、期待と異なるスクロールコンテナに紐づいている可能性があります。

scroll() は引数なしで書くと scroll(nearest block) が使われ、要素にとって最も近い祖先のスクロールコンテナが自動的に選ばれます。基準が不明確な場合は、明示的に scroll(root) または scroll(nearest) を指定する方が無難です。

チェック5:animation-fill-mode が指定されているか

スクロール位置がアニメーション範囲外にある場合、animation-fill-mode が初期値の none だと、アニメーション開始前の状態に戻ってしまいます。ページ読み込み時や、スクロール前の要素が見えない状態になったり、スクロール後に戻る可能性があります。

/* animation-fill-mode が省略されている(初期値: none) */
.element {
  animation: fade-in linear;
  animation-timeline: view();
}
/* これだと、スクロール前に要素が見えない状態になる可能性 */

/* 修正:両方向で状態を保持 */
.element {
  animation: fade-in linear both;
  animation-timeline: view();
}

チェック6:scroll()やview()の引数が正しいか

view() 関数の axis 引数(block / inline)がスクロール方向と一致しているか確認してください。垂直スクロールなら block、水平スクロールなら inline です。

また、view() の inset 値を指定している場合、単位が正しいか(px% か)も確認が必要です。

/* 垂直スクロール時の要素アニメーション */
.element {
  animation: fade-up linear both;
  animation-timeline: view(block);
}

/* 水平スクロール時(カルーセルなど)*/
.element {
  animation: fade-up linear both;
  animation-timeline: view(inline);
}

チェック7:スクロールコンテナに overflow スタイルが指定されているか

タイムラインの基準にしようとしている要素が、実際にスクロール可能になっているか確認してください。親要素に overflow: hiddenoverflow: auto が指定されていないと、スクロールコンテナとして機能しません。

/* 誤り:親要素がスクロール可能でない */
.parent {
  /* overflow が指定されていない */
}

.child {
  animation: slide linear both;
  animation-timeline: scroll(nearest);
}

/* 修正 */
.parent {
  overflow-y: auto;
}

Chrome DevToolsでのデバッグ方法

Chrome DevToolsの Animations パネルを使うことで、Scroll-driven Animationsの進行状況をリアルタイムで確認できます。

  1. DevToolsを開く(F12または右クリック → 検査)
  2. Elements パネルで対象要素を選択
  3. 右側の Computed Styles または Styles パネルで、animation-timeline プロパティが正しく適用されているか確認
  4. Computed Styles で animation-timeline を見つけ、その計算結果(例:view() の展開形)をチェック

Scroll-driven Animationsは時間ベースではないため、従来の Animations パネルでは秒数による進行状況が表示されません。代わりに、実際にページをスクロールしながら、要素のコンポーネント値(opacitytransform など)がどのように変わるかを確認するのが効果的です。

より詳細なデバッグ:コンソールでの確認

特定のスクロール位置をテストしたい場合は、コンソールで以下のようなスクリプトを実行して、スクロール位置を直接制御できます。

// ページスクロール位置を500pxに設定して動作を確認
window.scrollTo(0, 500);

// 特定の要素のスクロール位置を確認
const elem = document.querySelector('.carousel-container');
console.log(elem.scrollLeft);

transformとopacityを活用したカクつき(jank)を防ぐ軽量化手法

Scroll-driven Animationsはブラウザのコンポジター スレッドで処理されるため、基本的には高速です。ただし、アニメーション対象のプロパティ選択によって、パフォーマンスが大きく変わります。

コンポジター スレッドで処理されるプロパティ

transformopacity は、レイアウト計算(layout)や再描画(paint)をトリガーしないため、コンポジター スレッドだけで処理できます。スクロールイベント中にメインスレッドがブロックされていても、これらのアニメーションはスムーズに進行します。

/* 推奨:高速 */
.element {
  animation: slide linear both;
  animation-timeline: view();
}

@keyframes slide {
  from { transform: translateX(-100px); opacity: 0; }
  to { transform: translateX(0); opacity: 1; }
}

transform: translateX()transform: scale() で位置やサイズを変更し、opacity で透明度を制御するパターンが、パフォーマンスの観点から最適です。

メインスレッドでの処理が必要なプロパティ

一方、widthheightleftrightbackground-colorcolor などのプロパティを変更する場合、ブラウザはレイアウト計算や再描画を実行しなければならず、これはメインスレッドで処理されます。スクロール中にメインスレッドが忙しい場合、フレーム落ちやカクつきが発生する可能性が高まります。

/* 非推奨:カクつく可能性 */
.element {
  animation: resize linear both;
  animation-timeline: view();
}

@keyframes resize {
  from { width: 0; height: 0; }
  to { width: 100px; height: 100px; }
}

同じサイズ変更を実現する場合でも、transform: scale() を使う方が推奨されます。

実装上の最適化テクニック

プログレスバーのような単純な線の拡大であれば、次のように書き分けられます。

/* 非推奨:width を直接変更 */
.progress-bar {
  animation: bar-width linear both;
  animation-timeline: scroll();
}

@keyframes bar-width {
  from { width: 0; }
  to { width: 100%; }
}

/* 推奨:transform: scaleX() を使用 */
.progress-bar {
  animation: bar-scale linear both;
  animation-timeline: scroll();
  transform-origin: left;
}

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

背景色の段階的な変化が必要な場合も、できる限り opacitybackground-color の組み合わせで表現するか、複数の層を ::before::after で用意して、opacity だけを変更する工夫が有効です。

will-changeの活用

ブラウザにあらかじめ「このプロパティがアニメーション対象になる」と知らせることで、最適化のヒントを与えられます。

.element {
  will-change: transform, opacity;
  animation: slide linear both;
  animation-timeline: view();
}

ただし will-change は多用すると逆効果になるため、実際に効果があるかDevToolsで確認してから使用をお勧めします。

フィルターの使用上の注意

filter プロパティ(ぼかし、色調変更など)は、コンポジター スレッドで処理されず、描画負荷が高いです。スクロール中に filter を頻繁に変更するのは避けましょう。静的な装飾に留めるか、やむを得ず使う場合は事前にパフォーマンス計測を実施してください。

prefers-reduced-motionとCore Web Vitalsを考慮した実装

アニメーションが必ずしも全ユーザーにとって良い体験とは限りません。アクセシビリティとパフォーマンスの両立を考慮した実装が求められます。

prefers-reduced-motionによるモーション軽減

prefers-reduced-motion: reduce は、ユーザーがOS設定でモーションを減らすよう指定したことを示します。前庭障害(vestibular disorder)や、スクロール連動のアニメーションで不快感を感じるユーザーが、その旨をOSに伝えるための仕組みです。こうしたユーザーに対しては、アニメーションを止めるか、大幅に軽減する必要があります。

/* デフォルト:アニメーション有効 */
.reveal-item {
  animation: fade-up linear both;
  animation-timeline: view();
}

/* ユーザーがモーション軽減を指定している場合 */
@media (prefers-reduced-motion: reduce) {
  .reveal-item {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

あるいは、アニメーションは残すが、スクロール連動ではなく、時間ベースの簡単なフェードインに切り替える方法もあります。

@media (prefers-reduced-motion: reduce) {
  .reveal-item {
    animation: fade-only 0.6s ease-out forwards;
    animation-timeline: none; /* Scroll Timeline を無効化 */
  }

  @keyframes fade-only {
    from { opacity: 0; }
    to { opacity: 1; }
  }
}

prefers-reduced-motion のサポート状況は、Chrome、Safari、Firefox、Edgeの最新バージョンで一般的です。古いブラウザでは対応していないため、フォールバック(通常のアニメーション)が自動的に使われます。

Core Web Vitalsとの関連性

Core Web Vitalsは、Googleが定義したページ体験の重要指標で、Largest Contentful Paint(LCP)、Interaction to Next Paint(INP)、Cumulative Layout Shift(CLS)から構成されます。Scroll-driven Animationsがこれらに直接影響するわけではありませんが、実装方法によっては間接的に影響が出ます。

Cumulative Layout Shift(CLS)の観点

transformopacity のみを使うScroll-driven Animationsは、ビューポートの大きさやレイアウトの位置を変更しないため、CLSの悪化につながりません。ただし、widthheightleftmargin などでアニメーションを実装した場合、周囲の要素の位置がずれ、CLSが増加する可能性があります。

INP(Interaction to Next Paint)の観点

Scroll-driven Animationsはスクロール入力に反応しますが、メインスレッドで計算が発生しないため(transformopacity を使う限り)、INPに悪影響を及ぼしません。むしろ、JavaScriptの scroll イベントリスナーを使う場合よりも、INPが良くなる傾向があります。ただし、メインスレッドで処理が必要なプロパティを変更する場合は、スクロールの応答性が低下する可能性があるため注意が必要です。

Core Web Vitalsとの関係の誤解を避ける

「Scroll-driven Animationsを使えばCore Web Vitalsが自動的に改善される」という認識は誤りです。Core Web Vitalsが改善されるのは、transformopacity のみを使い、メインスレッドの負荷を抑えた実装を心がけている場合です。逆に実装方法が不適切だと、JavaScriptによる実装と比べて特に利点がありません。

パフォーマンス計測の実施

Scroll-driven Animationsの実装後は、以下のツールで実際のパフォーマンスを計測することをお勧めします。

  • Chrome DevTools のパフォーマンスプロファイラー:スクロール中のフレームレート(FPS)やメインスレッドのブロック時間を確認できます
  • Lighthouse:Core Web Vitals の各指標をシミュレーション環境で測定
  • Web Vitals JavaScript ライブラリ:ユーザーの実環境でのパフォーマンスデータを収集

特にモバイルデバイスでのテストが重要です。デスクトップでは問題なくても、低スペック端末ではフレーム落ちが発生する可能性があります。

不要なアニメーションの削除

パフォーマンス最適化の第一歩は、本当に必要なアニメーションだけを残すことです。次の質問に「いいえ」と答えるアニメーションは削除を検討してください。

  • そのアニメーションはユーザーにとって有益な情報を伝えているか?
  • そのアニメーションがなくても、ページの理解に支障はないか?
  • 複数のアニメーションが同時に進行しており、過度な視覚刺激になっていないか?

特にランディングページやマーケティングサイトでは、アニメーション過多になりやすいため、戦略的な取捨選択が必要です。

よくある質問(FAQ)

animation-durationを秒数で指定したら、アニメーションが期待通りに進みません

animation-duration はスクロール駆動型のアニメーションでは意味を持ちません。Scroll-driven Animationsでは、スクロール進行度がアニメーションの進捗を制御するため、秒数指定は無視されます。以下のように auto を指定するか、省略してください。

.element {
  animation: fade-in auto linear both; /* auto を明示 */
  animation-timeline: view();
}

/* または秒数を書かない */
.element {
  animation: fade-in linear both;
  animation-timeline: view();
}

秒数を指定してしまっても多くの場合動作しますが、コードを読む人が混乱するため、慣例に従って auto にしておくのが無難です。

複数の要素に同じCSSクラスを指定した場合、アニメーションは各要素で独立して動きますか?

はい、各要素が独立してアニメーションします。View Timelineを使う場合は特に明確で、各要素がスクロールコンテナのビューポートをどのように通過するかが個別に計算されるため、スクロール速度に関わらず、それぞれの要素が画面に入ってきたときに個別のアニメーションがトリガーされます。

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

この場合、4つの .reveal-card 要素があれば、スクロール速度がどうであれ、各カードが個別にアニメーションします。Scroll Timelineを使う場合(例:プログレスバー)でも、複数の要素に指定すれば各要素がスクロール位置に応じて独立して動きます。

名前付きタイムラインを使う利点は何ですか?

名前付きタイムラインは、スクロール親要素と、アニメーション対象要素が離れている場合に有効です。匿名の scroll()view() は要素の直近の祖先スクロールコンテナを自動選択しますが、複雑なレイアウトではこれが意図したコンテナと異なることがあります。名前付きタイムラインを使えば、明示的にどのスクロールコンテナを基準にするかを指定できます。

/* スクロールコンテナに名前を付ける */
.carousel-container {
  overflow-x: scroll;
  scroll-timeline: --carousel-scroll inline;
  scroll-timeline-name: --carousel-scroll;
}

/* 離れた場所の要素からそのタイムラインを参照 */
.carousel-indicator {
  animation: fill-bar linear both;
  animation-timeline: --carousel-scroll;
}

また、同じスクロールコンテナを基準に複数のアニメーションを制御したい場合も、名前付きタイムラインを使うと結果が予測しやすくなります。

Scroll-driven Animationsはスマートフォンでも同じように動きますか?

動きます。ただし、いくつか注意が必要です。

パフォーマンス:スマートフォンはデスクトップと比べてCPU・メモリが限られています。複数のアニメーションが同時に進行している場合、フレーム落ちが発生する可能性があります。特に低価格端末では注意が必要です。デスクトップで問題なくても、実機でのテストを推奨します。

タッチ操作のフリック:モバイルブラウザでのスクロール(特にiOSのmomentum scroll)は、デスクトップのマウスホイールと異なり、スクロール速度が変動します。このため、Scroll-driven Animationsの進行速度も変わり、ユーザーの期待と異なる挙動に見える可能性があります。

水平スクロール UI:カルーセルなど水平スクロール UIはスマートフォンでの操作性が低下しやすいため、タッチ友好的な設計を別途検討してください。

View Timelineの動作:View Timelineはビューポートとの相対位置を基準にするため、スマートフォンのビューポート(通常340〜390pxなど)が小さい場合、アニメーション範囲の計算結果がデスクトップと異なります。view-timeline-inset でタイミングを調整する際は、モバイルでもテストしてください。

/* モバイル対応の例 */
@media (max-width: 768px) {
  .reveal-item {
    animation: fade-up linear both;
    animation-timeline: view();
    animation-range: entry 10% entry 100%; /* モバイルではより早くトリガー */
  }
}

animation-rangeで 0px500px のようなピクセル単位を指定した場合、単位の意味は何ですか?

ピクセル単位は、スクロール位置の絶対値を表します。ページ上部(スクロール0)から、指定したピクセル数だけスクロールした時点がアニメーション範囲の開始・終了になります。

.element {
  animation: fade-in linear both;
  animation-timeline: scroll();
  animation-range: 200px 800px;
}

この場合、ページを200pxスクロールした時点でアニメーションが0%となり、800pxスクロール した時点で100%となります。パーセンテージ(20% 80%)との大きな違いは、タイムラインの全体の大きさに関わらず、常に同じスクロール位置で開始・終了するという点です。

ピクセル単位は Sticky Header のように「ページ上部から特定の距離」を基準にしたい場合に有効ですが、レスポンシブデザインではビューポート高さに応じて調整が必要になることがあります。

JavaScriptでスクロール位置を検出しながらScroll-driven Animationsを使うことはできますか?

できます。JavaScript と Scroll-driven Animations は独立して動作するため、組み合わせて使う場合も問題ありません。たとえば、JavaScriptでスクロール位置に応じてクラスを付与して条件付きスタイルを適用し、その上でScroll-driven Animationsを使うといった実装も可能です。

// JavaScriptでスクロール位置を監視
document.addEventListener('scroll', () => {
  if (window.scrollY > 500) {
    document.body.classList.add('scrolled');
  } else {
    document.body.classList.remove('scrolled');
  }
});
/* CSS でScroll-driven Animationsと条件付きスタイルを組み合わせ */
.element {
  animation: fade-in linear both;
  animation-timeline: view();
}

body.scrolled .element {
  /* 追加のスタイル */
  filter: brightness(1.2);
}

ただし、単純なスクロール連動アニメーション であればScroll-driven Animations だけで十分です。JavaScriptの追加は、CSSでは実現できない複雑な制御が必要な場合に限定することで、実装コストと保守性を最適化できます。

scroll() の scroller に self を指定する利点は何ですか?

self は、対象要素自身がスクロールコンテナである場合に使います。たとえば、横スクロールのカルーセル内でカード要素のアニメーションを制御したい場合、親要素である .carousel-track がスクロールコンテナであり、カード要素は .carousel-item です。カード自身はスクロール不可ですが、親のスクロール位置を基準にしたい場合に scroll(self) は使いません。むしろ scroll(nearest) が自動的に親を選択します。

実際に self が活躍するのは、要素自身に overflow: scroll を指定している場合です。

/* 親要素がスクロール可能 */
.scrollable-box {
  overflow-y: scroll;
  height: 300px;
}

/* 子要素のアニメーションをこの親要素のスクロールに紐づけたい */
.child-item {
  animation: reveal linear both;
  animation-timeline: scroll(nearest block);
}

上記の場合、nearest が自動的に .scrollable-box を選択するため、self を明示的に指定する必要はありません。self は、要素が自分自身のスクロール位置を基準にしたい(つまり、自分がスクロール可能である)という限定的な状況でのみ使われます。

animation-range を指定しない場合、デフォルトではアニメーションがいつ進行しますか?

デフォルトは animation-range: normal で、これはタイムラインの全範囲(0%〜100%)を意味します。

Scroll Timeline の場合:ページの最上部(スクロール 0%)からページの最下部(100%)まで、全スクロール範囲でアニメーションが進行します。

View Timeline の場合:対象要素がビューポートに見え始めてから(0%)、完全に見えなくなるまで(100%)のすべての過程でアニメーションが進行します。正確には cover 区間(要素が最初に見え始めてから最後に見えなくなるまで)です。

.element {
  animation: fade-in linear both;
  animation-timeline: view();
  /* animation-range を省略 = animation-range: normal
     = animation-range: cover 0% cover 100% と同義 */
}

より細かく制御したい場合(例:要素が画面に入ってくる過程だけでアニメーションを完了させたい)は、animation-range: entry 0% entry 100% のように明示的に指定します。

CSS Container Queries と Scroll-driven Animations を組み合わせることはできますか?

できます。Container Queries はメディアクエリの要素版で、親のコンテナサイズに応じてスタイルを変更できます。Scroll-driven Animations と組み合わせることで、コンテナサイズに応じてアニメーション方式を切り替えるといった実装が可能です。

/* コンテナクエリ対応ブラウザ */
@container (min-width: 600px) {
  .element {
    animation: large-screen-animation linear both;
    animation-timeline: view();
  }
}

@container (max-width: 599px) {
  .element {
    animation: small-screen-animation linear both;
    animation-timeline: view();
  }
}

ただし Container Queries の対応状況はまだ Scroll-driven Animations ほど広くないため、フォールバック処理を検討する必要があります。

animation-fill-mode を forwardsboth で指定した場合、どう違いますか?

forwardsboth の違いは、アニメーション開始前の状態をどう扱うかです。

animation-fill-mode: forwards :アニメーション完了後の最終状態(to 状態)を保持します。ただし、アニメーション開始前は要素の初期状態(from の状態ではなく、CSS で指定した初期値)に戻ります。

animation-fill-mode: both :アニメーション開始前は from 状態を、アニメーション完了後は to 状態を保持します。

Scroll-driven Animations では、スクロール位置がアニメーション範囲外のときにどのような見た目を保つかが重要になるため、通常は both を指定します。

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* animation-fill-mode: forwards の場合 */
.element {
  opacity: 0.5; /* デフォルト値 */
  animation: fade-in linear forwards;
  animation-timeline: view();
}

/* アニメーション範囲外では opacity: 0.5
   アニメーション完了後は opacity: 1 */

/* animation-fill-mode: both の場合 */
.element {
  opacity: 0.5;
  animation: fade-in linear both;
  animation-timeline: view();
}

/* スクロール前は opacity: 0(from 状態)
   アニメーション完了後は opacity: 1(to 状態) */

View Timeline での要素の出現アニメーションでは、スクロール前(要素がビューポート外)の状態を from で定義することが多いため、both の指定が適切です。

まとめ

Scroll-driven Animationsは、単なる新しいCSS機能ではなく、スクロール表現における考え方そのものを変えます。これまでJavaScriptなしに実装できなかった「スクロール位置に応じたリアルタイムアニメーション」が、CSSだけで実現できるようになったことで、実装コストの削減と保守性の向上が同時に実現されました。

ここまで見てきたように、Scroll-driven Animationsの全体像は、実はそこまで複雑ではありません。Scroll Timelineとview Timelineという2つのタイムライン概念を理解し、animation-timelineanimation-range の2つのプロパティの使い方を押さえれば、多くの実装ケースに対応できます。動かない場合のデバッグも、チェックリストに沿って確認すれば原因を特定しやすくなります。

ただ、テクニック的な習得よりも重要なのは「何にScroll-driven Animationsを使うべきか」という判断です。むしろ何に使わないかを決めることが、実務での成功につながります。複雑な条件分岐が必要な場合、複数要素の連携が必要な場合、スクロール以外のイベントも考慮する必要がある場合は、JavaScriptやGSAPの方が適切です。Scroll-driven Animationsの出番は、スクロール位置とCSS アニメーション進度の関係がシンプルで、メインスレッドの負荷を避けたい場面です。

重要ポイント

  • Scroll Timeline はページ全体のスクロール進度を、View Timeline は対象要素がビューポートを通過する過程を基準にする
  • animation-timelineanimation ショートハンドより後に記述する。前に書くと上書きされてしまう
  • animation-fill-mode: both はスクロール範囲外での要素の見た目を保つために必須。省略すると予期しない挙動になる
  • パフォーマンスを最適化するには transformopacity に限定し、メインスレッド処理が必要なプロパティは避ける
  • @supports (animation-timeline: view()) で機能検出を行い、非対応ブラウザへのフォールバックを用意する
  • prefers-reduced-motion: reduce に対応し、モーション軽減を指定するユーザーへの配慮を忘れない

2026年現在、ブラウザ対応も85%を超えており、新規プロジェクトではScroll-driven Animationsを実装の選択肢に入れるのが現実的です。古い環境への対応が必須でなければ、CSSオンリーで実装できる気軽さと、メインスレッドに負荷をかけない利点は無視できません。

実装に際しては、プログレスバーやカード型フェードインなど、シンプルなパターンから始めるのをお勧めします。基本的な動作パターンを把握した後は、複数の要素の組み合わせやカスタム範囲指定など、より複雑な表現へも応用しやすくなります。記事で紹介したコード例はすべてそのままブラウザで動作するため、手元で試しながら仕組みを理解することが最短ルートになるはずです。

スクロール連動表現の実装において、選択肢が増えることは実装者にとって大きなメリットです。状況に応じて最適な手法を選べるようになることで、ユーザー体験の質も、実装の効率性も向上していきます。

タイトルとURLをコピーしました