CSS Scroll Timeline: 新しいスクロール体験の実現
スクロール駆動アニメーションは、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; }
}
アニメーションさせるプロパティ
transform と opacity だけに絞ってください。 スクロール駆動はスクロールのたびに値が更新されるため、width や top を動かすとレイアウト再計算が連続で走り、通常のアニメーション以上に重くなります。
装飾要素はアクセシビリティツリーから外す
進捗バーのような純粋な装飾には 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-name+timeline-scope - 動かすのは
transformとopacityだけ。prefers-reduced-motionに必ず対応する
「CSSだけでスクロールアニメーションが書ける」のは事実ですが、2026年時点ではまだ全ブラウザで動く技術ではありません。土台をJavaScriptで作り、対応ブラウザだけ滑らかにする——この順番で組むのが安全です。