スクロール駆動アニメーションは、animation-timeline: scroll()animation-timeline: view() の2つで書きます。JavaScriptを1行も使わずに、スクロール量に完全追従するアニメーションが作れます。

この記事は2024年の公開後、2026年9月に全面的に書き直しました。 公開当時のコードで使っていた @scroll-timeline というアットルールは仕様から削除され、どのブラウザでも動作しません。現行仕様のプロパティに差し替えたうえで、対応状況とフォールバックの考え方を追記しています。

  • @scroll-timeline { source: ...; scroll-source: ...; } は無効です。策定途中で廃案になった構文で、貼り付けても何も起きません
  • 現行仕様は scroll-timeline / view-timeline プロパティと animation-timeline の組み合わせです

スクロール駆動アニメーションとは何か

通常の CSS アニメーションは時間で進みます。duration: 1s と書けば1秒かけて再生されます。

スクロール駆動アニメーションは、その進行の基準を時間からスクロール位置に置き換えます。スクロールを止めればアニメーションも止まり、戻せば巻き戻ります。

通常のアニメーション スクロール駆動
進行の基準 時間 スクロール量
途中で止まるか 止まらない スクロールを止めれば止まる
巻き戻し しない 上にスクロールすると戻る
duration 必要 無視される

タイムラインには2種類あります。この使い分けが最初のポイントです。

  • scroll():スクロールコンテナ全体の進行度。ページ上部の読書進捗バーなど
  • view():その要素がビューポートを横切る進行度。要素ごとの出現アニメーションなど

 

対応状況とフォールバックの前提

2026年9月時点で Baseline には入っていません。 先に対応状況を確認しておきます。

ブラウザ 対応
Chrome / Edge 115以降で対応
Safari 26で対応
Firefox 既定で無効layout.css.scroll-driven-animations.enabled フラグの内側)

したがって「これ単体で組む」のではなく、段階的強化として上乗せする」のが実務での使い方になります。分岐には @supports を使います。

@supports (animation-timeline: view()) {
  /* 対応ブラウザだけスクロール駆動にする */
}

 

非対応ブラウザ向けの土台は、Intersection Observer API を使ったスクロールアニメーションで作ります。非対応環境で「要素が透明のまま消える」のが最悪の結果なので、そこだけは必ず避けてください。

 

実装例

See the Pen
css_scrollTimeline01
by Rin (@rinblog0408)
on CodePen.

 

scroll():ページの読書進捗バーを作る

最も分かりやすい例が、ページ最上部に置く進捗バーです。これがJavaScriptなしで書けます。

<div class="progress-bar" aria-hidden="true"></div>

 

.progress-bar {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  height: 4px;
  background: #2a7fff;
  transform-origin: 0 50%;
  transform: scaleX(0);
  z-index: 100;
}

@supports (animation-timeline: scroll()) {
  .progress-bar {
    animation: grow-progress linear both;
    /* ルートスクローラーの縦方向を基準にする */
    animation-timeline: scroll(root block);
  }
}

@keyframes grow-progress {
  to { transform: scaleX(1); }
}

 

scroll() の引数は「どのスクローラーの、どの軸か」です。

書き方 意味
scroll() 最も近い祖先のスクローラー、ブロック方向
scroll(root) ページ全体のスクロール
scroll(root block) ページ全体の縦スクロール(縦書きなら横)
scroll(nearest inline) 直近スクローラーの横スクロール
scroll(self block) その要素自身のスクロール

アニメーションさせるのは transform: scaleX() にしてください。 width を動かすとスクロールのたびにレイアウトが再計算され、カクつきの原因になります。

 

view():要素の出現に合わせてアニメーションする

使用頻度が高いのはこちらです。その要素がビューポートに入り、抜けるまでの進行度を基準にします。

.card {
  opacity: 1;
  transform: none;
}

@supports (animation-timeline: view()) {
  .card {
    animation: fade-in-up linear both;
    animation-timeline: view();
    /* 下から入り始めて40%進んだ時点で完了させる */
    animation-range: entry 0% entry 40%;
  }
}

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

 

animation-range の指定が要です。 省略すると「要素が下から入り始めてから上に抜けきるまで」の全区間が使われ、画面中央にある間もずっとアニメーションが進み続けます。entry の区間に限定することで、「入ってきたときにふわっと出る」動きになります。

範囲の名前 いつからいつまで
entry 下端から現れ始め、完全に入りきるまで
exit 上端から消え始め、完全に抜けるまで
cover 現れ始めてから抜けきるまでの全区間
contain 要素全体がビューポートに収まっている区間
entry-crossing / exit-crossing 要素が境界線をまたいでいる区間

初期状態に opacity: 0 を書いていない点にも注目してください。@supports の外では通常表示のままにしておくことで、Firefox などの非対応環境でもコンテンツが読めます。

 

画像のズームとパララックス

view()cover を組み合わせると、スクロール量に追従するズームが書けます。

.hero-image {
  overflow: hidden;
}

@supports (animation-timeline: view()) {
  .hero-image img {
    animation: zoom-out linear both;
    animation-timeline: view();
    animation-range: cover 0% cover 100%;
  }
}

@keyframes zoom-out {
  from { transform: scale(1.25); }
  to   { transform: scale(1); }
}

 

スクロールを戻すと拡大率も戻るので、JavaScriptのパララックス実装にありがちな「戻したときにズレる」問題が起きません

 

離れた要素をタイムラインの基準にする

タイムラインは既定では祖先方向にしか探索されません。兄弟要素や離れた要素を基準にしたい場合は、名前を付けて timeline-scope で共有します。

/* 基準にする側 */
.article-body {
  view-timeline-name: --article;
  view-timeline-axis: block;
}

/* 共通の祖先で名前を公開する */
.layout {
  timeline-scope: --article;
}

/* 離れた場所にある側 */
.sidebar-indicator {
  animation: grow-progress linear both;
  animation-timeline: --article;
}

 

timeline-scope を書き忘れると、アニメーションが静かに何も起きないだけでエラーも出ません。動かないときはまずここを疑ってください。

 

必ず入れるべき配慮

prefers-reduced-motion

スクロールに完全追従する動きは、視差効果に敏感なユーザーにとって負担になります。これは必須の対応です。

@media (prefers-reduced-motion: reduce) {
  .card,
  .hero-image img,
  .progress-bar {
    animation: none;
  }
  .progress-bar { display: none; }
}

 

アニメーションさせるプロパティ

transformopacity だけに絞ってください。 スクロール駆動はスクロールのたびに値が更新されるため、widthtop を動かすとレイアウト再計算が連続で走り、通常のアニメーション以上に重くなります。

装飾要素はアクセシビリティツリーから外す

進捗バーのような純粋な装飾には aria-hidden="true" を付けます。スクリーンリーダーに読み上げる意味がありません。

 

JavaScriptによる実装との使い分け

やりたいこと 選ぶもの
スクロール量に追従する連続アニメーション CSS animation-timeline
一度だけ再生する出現アニメーション Intersection Observer
完了を待って次の処理をしたい Web Animations API
スクロール位置に応じた要素の追加読み込み Intersection Observer(無限スクロール)

一度きりの「ふわっと出す」だけなら、Intersection Observer の方が対応範囲が広く確実です。 スクロール駆動アニメーションが本領を発揮するのは、進捗バーやパララックスのようにスクロール量と表示が連動し続ける表現です。

 

まとめ

  • @scroll-timeline アットルールは廃案。動きません。現行は animation-timeline プロパティ
  • コンテナ全体の進行は scroll()、要素の出現は view()
  • view() では animation-range: entry 0% entry 40% のように区間を指定する
  • duration は無視される。animation: name linear both; の形で書く
  • Firefox は既定で無効。 @supports (animation-timeline: view()) で段階的強化にする
  • 非対応環境でコンテンツが消えないように、初期状態は通常表示にしておく
  • 離れた要素を基準にするには view-timeline-nametimeline-scope
  • 動かすのは transformopacity だけ。prefers-reduced-motion に必ず対応する

「CSSだけでスクロールアニメーションが書ける」のは事実ですが、2026年時点ではまだ全ブラウザで動く技術ではありません。土台をJavaScriptで作り、対応ブラウザだけ滑らかにする——この順番で組むのが安全です。

ABOUT ME
りん
このブログでは、Web開発やプログラミングに関する情報を中心に、私が日々感じたことや学んだことをシェアしています。技術と生活の両方を楽しめるブログを目指して、日常で触れた出来事や本、グルメの話題も取り入れています。気軽に覗いて、少しでも役立つ情報や楽しいひとときを見つけてもらえたら嬉しいです。