CSS REFERENCE

インタラクティブ

scroll-timeline 要素

ー スクロール位置をアニメーションの「時間軸」にする、JavaScript不要のスクロール連動演出

POINT !

scroll-timeline 要素のポイント!

  • scroll-timeline は CSS の Scroll-driven Animations 仕様の一部。スクロール位置を animation の時間軸として使うことで、JavaScript の scroll イベントを使わずにスクロール連動アニメーションが実現できる。Chrome 115以降でサポート(2025年現在、Firefox / Safari は未対応または実験的)。
  • animation-timeline プロパティで scroll() または view() を使う方法と、scroll-timeline-name で名前付きタイムラインを定義して animation-timeline で参照する方法の2種類がある。scroll() はスクロールコンテナ全体の位置、view() は要素がビューポートに入る/出るタイミングを基準にする。
  • view-timeline と animation-timeline: view() の組み合わせを使うと「スクロールで要素がビューポートに入ったときにアニメーション開始」という、Intersection Observer で実装していた演出が CSS だけで書ける。
  • animation-range プロパティで、アニメーションを開始・終了するスクロール位置の範囲を entry / exit / cover / contain などのキーワードで制御できる。これにより「要素が半分見えたら開始」などの細かい制御が可能。

概要

scroll-timeline は CSS Scroll-driven Animations 仕様(Chrome 115以降で実装)が提供する革新的なプロパティです。従来 JavaScript の scroll イベントや Intersection Observer で実装していた「スクロール連動アニメーション」を、純粋な CSS だけで実現できます。

【仕組みの核心】
CSSのanimationは通常「時間」で進みますが、Scroll-driven Animationsでは「スクロール位置」がアニメーションの進行を制御します。ページを下にスクロールするにつれてアニメーションが進み、上にスクロールすると逆再生されます。

【2種類のタイムライン】
① scroll-timeline(スクロールタイムライン)
スクロールコンテナの「全体のどこにいるか」が軸になります。
ページのプログレスバー(上部に表示するスクロール進行インジケーター)が最もシンプルな例です。

② view-timeline(ビュータイムライン)
「対象要素がビューポートに対してどのくらい見えているか」が軸になります。
要素がビューポートに入ってきたときにフェードインするアニメーションなど、intersection-observer-like な演出に使います。

Chromeのみの対応状況を考慮し、@supports でのフォールバック実装が実務では必須です。

具体的な役割

ページ上部のスクロール進捗バー、スクロールで要素がフェードイン・スライドインする入場演出、パララックス効果、スクロール連動のSVGパスアニメーション、ストーリーテリング系ランディングページなど。

アクセシビリティ

♿ scroll-timeline によるアニメーションも、通常の animation と同様に prefers-reduced-motion への対応が必要です。

スクロール連動アニメーションは特に、前庭障害のあるユーザーがスクロールした際に画面上の多くの要素が動くことで体調不良を引き起こすリスクが高いため、@media (prefers-reduced-motion: reduce) での完全な無効化を検討してください。

また、スクロール自体のアクセシビリティとして:
・キーボードのみでナビゲーション可能であること
・スクリーンリーダーがスクロール位置に関係なくコンテンツを読み上げられること

これらは scroll-timeline の有無にかかわらず保証する必要があります。

セットで使うプロパティ

📦 scroll-timeline は @scroll-timeline ルール(または scroll-timeline-name / scroll-timeline-axis ショートハンドプロパティ)でタイムラインを定義し、animation-timeline プロパティで要素の animation に紐付けて使います。

【匿名タイムラインの使い方(最もシンプル)】
animation-timeline: scroll() ← ページ全体のスクロールを軸にする
animation-timeline: view() ← 要素のビューポートへの出入りを軸にする

【名前付きタイムライン(特定の要素を参照する場合)】
.scroller {
scroll-timeline-name: --my-timeline;
scroll-timeline-axis: block; /* block(縦)/ inline(横) */
}
.animated {
animation-timeline: --my-timeline;
}

【animation-range(開始/終了位置の制御)】
animation-range: entry 0% entry 100%; /* 要素が入ってくる間 */
animation-range: cover 20% cover 80%; /* 要素が画面を覆う間の20〜80% */

技術の変遷:昔の常識 vs 今の常識

TRADITION (昔)
スクロール連動アニメーションは JavaScript の window.addEventListener('scroll', ...) でスクロール量を取得し、CSSの transform や opacity を書き換えることで実装していた。throttle や requestAnimationFrame での最適化が必要で、コードが複雑になりがちだった。Intersection Observer の登場でビューポート検出は改善されたが、依然として JavaScript が必要だった。
MODERN (今)
CSS Scroll-driven Animations(Chrome 115+)により、scroll-timeline と view-timeline の組み合わせで JavaScript ゼロのスクロール連動アニメーションが可能になった。ブラウザのコンポジットスレッドで処理されるため、メインスレッドのjankを避けられる利点もある。未対応ブラウザへは @supports でのフォールバック実装が推奨。

関連する値

値 / 関連 説明
scroll() 匿名スクロールタイムライン。ページ全体または指定スクロールコンテナのスクロール位置をタイムラインにする
view() 匿名ビュータイムライン。要素自身がビューポートへ出入りする進行度をタイムラインにする
scroll-timeline-name 名前付きスクロールタイムラインの定義。--my-name のようにカスタムプロパティ形式で命名する
scroll-timeline-axis スクロール方向の指定。block(縦スクロール、デフォルト)/ inline(横スクロール)
animation-range アニメーションの開始・終了位置。entry / exit / cover / contain + パーセントで細かく制御できる
ブラウザ対応 Chrome 115以降・Edge 115以降でサポート。Firefox・Safariは2025年時点で未対応または実験的。@supports でのフォールバック必須
継承 なし

利用例

ページスクロール進捗バー

ページをスクロールするとヘッダーの下のバーが伸びる進捗インジケーター。scroll-timeline の最もシンプルな実用例。

HTML Structure
<style>@keyframes grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}.progress-bar{position:sticky;top:0;left:0;right:0;height:5px;background:linear-gradient(90deg,#1a56db,#7c3aed);transform-origin:left center;border-radius:0 4px 4px 0;animation:grow linear;animation-timeline:scroll(root)}</style><div style="height:400px;overflow-y:auto;background:#f8fafc;border-radius:12px;border:1px solid #e2e8f0" id="scrollDemo"><div class="progress-bar"></div><div style="padding:24px;font-family:sans-serif"><p style="font-size:14px;color:#64748b;margin-bottom:12px">👇 この枠の中をスクロールしてみて</p><div style="display:flex;flex-direction:column;gap:16px"><div style="background:white;border-radius:8px;padding:18px;box-shadow:0 1px 4px rgba(0,0,0,0.06)"><div style="font-weight:700;margin-bottom:6px;font-size:14px">セクション 1</div><div style="font-size:13px;color:#94a3b8;line-height:1.6">スクロール位置に連動してバーが伸びていきます。</div></div><div style="background:white;border-radius:8px;padding:18px;box-shadow:0 1px 4px rgba(0,0,0,0.06)"><div style="font-weight:700;margin-bottom:6px;font-size:14px">セクション 2</div><div style="font-size:13px;color:#94a3b8;line-height:1.6">スクロール位置に連動してバーが伸びていきます。</div></div><div style="background:white;border-radius:8px;padding:18px;box-shadow:0 1px 4px rgba(0,0,0,0.06)"><div style="font-weight:700;margin-bottom:6px;font-size:14px">セクション 3</div><div style="font-size:13px;color:#94a3b8;line-height:1.6">スクロール位置に連動してバーが伸びていきます。</div></div><div style="background:white;border-radius:8px;padding:18px;box-shadow:0 1px 4px rgba(0,0,0,0.06)"><div style="font-weight:700;margin-bottom:6px;font-size:14px">セクション 4</div><div style="font-size:13px;color:#94a3b8;line-height:1.6">スクロール位置に連動してバーが伸びていきます。</div></div><div style="background:white;border-radius:8px;padding:18px;box-shadow:0 1px 4px rgba(0,0,0,0.06)"><div style="font-weight:700;margin-bottom:6px;font-size:14px">セクション 5</div><div style="font-size:13px;color:#94a3b8;line-height:1.6">スクロール位置に連動してバーが伸びていきます。</div></div><div style="background:white;border-radius:8px;padding:18px;box-shadow:0 1px 4px rgba(0,0,0,0.06)"><div style="font-weight:700;margin-bottom:6px;font-size:14px">セクション 6</div><div style="font-size:13px;color:#94a3b8;line-height:1.6">スクロール位置に連動してバーが伸びていきます。</div></div><div style="background:white;border-radius:8px;padding:18px;box-shadow:0 1px 4px rgba(0,0,0,0.06)"><div style="font-weight:700;margin-bottom:6px;font-size:14px">セクション 7</div><div style="font-size:13px;color:#94a3b8;line-height:1.6">スクロール位置に連動してバーが伸びていきます。</div></div><div style="background:white;border-radius:8px;padding:18px;box-shadow:0 1px 4px rgba(0,0,0,0.06)"><div style="font-weight:700;margin-bottom:6px;font-size:14px">セクション 8</div><div style="font-size:13px;color:#94a3b8;line-height:1.6">スクロール位置に連動してバーが伸びていきます。</div></div></div></div></div>
CSS Style
@keyframes grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  background: #1a56db;
  transform-origin: left center;

  /* スクロール全体をタイムラインに */
  animation: grow linear;
  animation-timeline: scroll();
}

/* 未対応ブラウザへのフォールバック */
@supports not (animation-timeline: scroll()) {
  .progress-bar { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .progress-bar { display: none; }
}

スクロールで要素がフェードイン(view-timeline)

要素がビューポートに入ってきたときにアニメーションを開始する。Intersection Observer の CSS版。

HTML Structure
<style>@keyframes fadeInUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}.reveal{animation:fadeInUp linear both;animation-timeline:view();animation-range:entry 0% entry 80%}@supports not (animation-timeline:view()){.reveal{opacity:1;transform:none}}</style><div style="height:320px;overflow-y:auto;background:#f1f5f9;border-radius:12px;padding:20px;font-family:sans-serif"><p style="font-size:13px;color:#64748b;margin-bottom:16px">👇 スクロールしてカードを表示</p><div style="display:flex;flex-direction:column;gap:14px;padding-top:8px"><div class="reveal" style="background:white;border-radius:10px;padding:18px;box-shadow:0 2px 8px rgba(0,0,0,0.06)"><div style="font-weight:700;font-size:14px;margin-bottom:4px">アイテム 1</div><div style="font-size:12px;color:#94a3b8">スクロールで登場します ✨</div></div><div class="reveal" style="background:white;border-radius:10px;padding:18px;box-shadow:0 2px 8px rgba(0,0,0,0.06)"><div style="font-weight:700;font-size:14px;margin-bottom:4px">アイテム 2</div><div style="font-size:12px;color:#94a3b8">スクロールで登場します ✨</div></div><div class="reveal" style="background:white;border-radius:10px;padding:18px;box-shadow:0 2px 8px rgba(0,0,0,0.06)"><div style="font-weight:700;font-size:14px;margin-bottom:4px">アイテム 3</div><div style="font-size:12px;color:#94a3b8">スクロールで登場します ✨</div></div><div class="reveal" style="background:white;border-radius:10px;padding:18px;box-shadow:0 2px 8px rgba(0,0,0,0.06)"><div style="font-weight:700;font-size:14px;margin-bottom:4px">アイテム 4</div><div style="font-size:12px;color:#94a3b8">スクロールで登場します ✨</div></div><div class="reveal" style="background:white;border-radius:10px;padding:18px;box-shadow:0 2px 8px rgba(0,0,0,0.06)"><div style="font-weight:700;font-size:14px;margin-bottom:4px">アイテム 5</div><div style="font-size:12px;color:#94a3b8">スクロールで登場します ✨</div></div><div class="reveal" style="background:white;border-radius:10px;padding:18px;box-shadow:0 2px 8px rgba(0,0,0,0.06)"><div style="font-weight:700;font-size:14px;margin-bottom:4px">アイテム 6</div><div style="font-size:12px;color:#94a3b8">スクロールで登場します ✨</div></div></div></div>
CSS Style
@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.reveal {
  animation: fadeInUp linear both;

  /* 要素自身のビューポートへの出入りを軸に */
  animation-timeline: view();

  /* 要素がビューポートに入り始めてから完全に入るまでの間 */
  animation-range: entry 0% entry 100%;
}

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

よくある誤用・注意点

scroll-timeline を animation-duration と組み合わせてしまうのは誤り。スクロールタイムラインを使う際、animation-duration は無視される(または 'auto' にする必要がある)。また、ブラウザ対応を確認せずに本番環境で使うと、非対応ブラウザでアニメーションが壊れる。

HTML Structure
CSS (Incorrect)
/* ❌ 悪い例:duration を指定してもスクロールタイムラインでは無意味 */
.bar {
  animation: grow 2s linear; /* 2s は scroll() タイムラインでは無視される */
  animation-timeline: scroll();
}

/* ❌ 悪い例:@supports なしで本番使用(Firefox/Safari で無視される) */
.card {
  animation: fadeIn linear;
  animation-timeline: view();
  /* Firefoxでは animation-timeline が未対応のため
     animation 全体が無効になる可能性あり */
}

/* ✅ 良い例:@supports でフォールバックを用意 */
.card {
  opacity: 1; /* デフォルトは普通に表示 */
}
@supports (animation-timeline: view()) {
  .card {
    animation: fadeIn linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 80%;
  }
}