これまでの記事では、CSSやJavaScriptを使ってアニメーションの実装方法を紹介しました。
@keyframesやanimation、transition、Intersection Observerって何?という方はまずは下記の記事をご確認下さい。
これらの基本を押さえたうえで、
今回は「Scroll-driven Animations」と呼ばれる
Chrome発の標準CSSスクロールアニメーション機能について解説します。
目次
Scroll-driven Animations とは?
従来との違いと対応ブラウザについて
Scroll-driven Animations(スクロール連動アニメーション) は、
Chrome 115以降(2023年7月~)で実装された、CSSのみで「スクロール位置に連動したアニメーション」を記述できる新しい標準仕様です。
これまでは、
- 「ページをどれだけスクロールしたか」
- 「要素がどこまで画面に入ってきたか」
といった情報を JavaScript で計算して、CSS に反映する 必要がありましたが、
Scroll-driven Animations を使うと、CSS だけの数行で「スクロール進行度に応じたアニメーション」が書けます。
また、同じ見た目を JavaScript でやるより処理負担が軽く・滑らかになりやすいと言われています。(※どれくらいかは実際に計測したわけではありません)
対応ブラウザの状況
これまで Safariでは非対応でしたが、Safari(macOS / iOS ともに)26 以降(2025年9月〜)から対応されたことでFirefox系以外はほぼ主要ブラウザで対応されている状況になっています。
https://caniuse.com/wf-scroll-driven-animations
2025年末時点のざっくりした対応状況です。
- Chrome / Edge(Chromium 系)
- 115 以降で対応(デスクトップ・Android ともに OK)
- Safari(macOS / iOS)
- Safari 26(iOS 26 を含む)以降で対応
- Firefox
- まだ実験的扱いで、
about:configからlayout.css.scroll-driven-animations.enabledをtrueにすると試せる段階(参考)
- まだ実験的扱いで、
「Chrome / Edge / 新しい Safari / 新しめのモバイルブラウザはほぼ対応。Firefox 系だけまだ様子見」
という状態です。
基本の書き方とサンプル
タイムラインについて
まず押さえておきたいのが「タイムライン」という考え方です。
通常の CSS アニメーションは、暗黙的に 時間ベースのタイムライン を使っています。
時間ベースのタイムラインとは、「経過した時間」に応じて 0% → 100% で進んでいくもの です。
例えば
.box {
animation: fade-in 1s ease-out forwards;
}
と書いた場合は、
- 再生開始から 0.5 秒経った時点がタイムラインの 50%
- 1 秒経った時点で 100% に到達してアニメーション終了
という「時間だけで決まる」タイムラインが暗黙に使われています。
この「時間ベース」が、Scroll-driven Animations では「スクロールベースのタイムライン」に置き換わる、というイメージです。
animation-timelineとタイムラインについて
animation-timeline
どのタイムラインを使ってアニメーションを進めるかを設定するのが animation-timeline です。
そして、Scroll-driven Animations ではスクロール連動のタイムラインが2種類あります。
scroll-timeline 系
スクロール量(何 px / 何 % スクロールしたか)に連動するタイムライン
- CSS 関数:
scroll() - 関連プロパティ:
scroll-timeline-name,scroll-timeline-axis,scroll-timeline(ショートハンド)
view-timeline 系
要素が画面の中を通過していく様子に連動するタイムライン
- CSS 関数:
view() - 関連プロパティ:
view-timeline-name,view-timeline-axis,view-timeline-inset,view-timeline(ショートハンド)
次に、この2つのタイムラインの基本的な使い方について見ていきます。
scroll-timeline スクロール量に連動
scroll() は「スクロール量」でアニメーションを動かします。
まずはデモをご覧下さい。下にスクロールすると、画面上部のプログレスバーがスクロールする分、伸びていきます。
<body>
<div class="scroll-progress"></div>
<main class="content">
<h1>Scroll-driven Animations のテスト</h1>
<p>下にスクロールしてみてください。</p>
<p>上部のプログレスバーがスクロールする分伸びているのがわかります。</p>
</main>
</body>
/* スクロールする高さを確保 */
.content {
min-height: 200vh; /* 2画面分 */
}
/* 上部に固定する細いバー */
.scroll-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 4px;
background: #f25b5b;
transform-origin: left center;
transform: scaleX(0); /* 最初は 0% */
}
/* バーが 0% → 100% に伸びるアニメーション */
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
/* 通常の CSS アニメーションの指定 */
.scroll-progress {
animation-name: grow-progress;
animation-duration: 1s;
animation-timing-function: linear;
animation-fill-mode: both;
}
/* Scroll-driven Animations に対応したブラウザだけ、この中身が効く */
@supports (animation-timeline: scroll()) {
.scroll-progress {
/* 「ページのスクロール位置」を、このアニメーションのタイムラインに使う */
animation-timeline: scroll(root block);
}
}
デモのコード解説
まず、mainの.contentにスクロールするための高さと、.scroll-progressで上部に固定する細いバーを定義しています。
.content {
min-height: 200vh; /* 2画面分 */
}
.scroll-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 4px;
background: #f25b5b;
transform-origin: left center;
transform: scaleX(0); /* 最初は 0% */
}
次に「0% → 100% に伸びる」アニメーションを定義しています。
@keyframes grow-progress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
/* 通常の CSS アニメーションの指定 */
.scroll-progress {
animation-name: grow-progress;
animation-duration: 1s;
animation-timing-function: linear;
animation-fill-mode: both;
}
このままですと、これまでの通りの「時間ベースで 1秒かけて横方向に 0→1 に伸びるだけ」です。
が、そのアニメーションを「スクロールで進める」ように切り替えるanimation-timelineを定義しています。
/* Scroll-driven Animations に対応したブラウザだけ、この中身が効く */
@supports (animation-timeline: scroll()) {
.scroll-progress {
/* 「ページのスクロール位置」を、このアニメーションのタイムラインに使う */
animation-timeline: scroll(root block);
}
}
@supports (animation-timeline: scroll()) { }
→Scroll-driven Animations に対応しているブラウザだけ、この中身を使います。
→古いブラウザもまだまだ残っていますので、現状では@supportsを入れておくのが賢明です。
animation-timeline: scroll(root block);
→ 「ページ全体(root)の縦スクロール量(block)に合わせて、このアニメーションを進めてください」という指定です。
ページの一番上が 0%、一番下までスクロールしたときに 100% になります。
結果として、animation-timeline: scroll(root block); を1行入れただけで、javascriptを利用しなくてもCSSだけで、
- ページの一番上:バーが 0%(見えない)
- ページの一番下:バーが 100% まで伸びる
というスクロール量に応じたバーの長さの挙動が実現できます。
animation-timeline: scroll(root block); について
scroll() のカッコの中は、ざっくりこういうイメージで覚えておくと楽です。
root… ページ全体(html)を基準にするself… その要素自身が入っているスクロールエリアを基準にするblock… 縦方向(ふつうの縦スクロール)inline… 横方向(横スクロール)
事例:
scroll(root block)
→ 「ページ全体の縦スクロールに合わせて進める」scroll(self block)
→ 「この要素が入っている箱(スクロールコンテナ)の縦スクロールに合わせて進める」
最初のうちは「root block(ページ全体の縦スクロール)」だけ使えればOKだと思います。
view-timeline 要素が画面に入ったら
view() は「この要素が、画面の中にどれくらい見えているか」を元にアニメーションを動かします。
イメージとしては、Intersection Observer でやっていた「画面に入ったらクラスをつけて徐々に大きくなってフェードイン」を、スクロール連動させてやる感じです。(CSSだけで)
まずはデモをご覧下さい。
スクロールしていき、要素が画面に入ると徐々に大きく&はっきり表示されるようになっていき、
要素が画面の50%まできたら、画像の大きさが100%の大きさになって動きが終了します。
<div class="spacer"></div>
<section class="fade-zoom">
<img class="fade-zoom__img" src="valmon.png">
</section>
.fade-zoom__img {
max-width: 100%;
height: auto;
}
/* フェード+ズーム用の keyframes */
@keyframes fadeZoomIn {
0% {
opacity: 0;
transform: scale(0);
}
100% {
opacity: 1;
transform: scale(1);
}
}
.fade-zoom {
/* 最初は 縮小+透明 */
opacity: 0;
transform: scale(1);
/* アニメーション */
animation-name: fadeZoomIn;
animation-timing-function: ease-out;
animation-fill-mode: both;
animation-duration: 1s; /* 値自体はダミー */
}
/*
* Scroll-driven Animations 対応ブラウザ向け
* animation-timeline: view(block); を使って
* 「画面に入ったらスクロール進行に応じてフェード+ズーム」させる
*/
@supports (animation-timeline: view(block)) {
.fade-zoom {
animation-timeline: view(block);
animation-range: entry 0% cover 50%;
}
}
animation-timeline: view(block);
まず、この1行で時間ではなくスクロール位置でアニメーションが進むようになります。
view … この要素が「画面(ビューポート)の中を通過する様子」をタイムラインにする、という意味
block … 上下方向(縦スクロール)を基準にする
animation-range: entry 0% cover 50%;
アニメーションを適用させる範囲を定義しています。
1つ目はanimation-range-startで、2つ目はanimation-range-endのショートハンドです。
animation-range-start はタイムラインの始まり、 animation-range-endはタイムラインの終わりです。
entry 0% → 要素が画面に入り始めた瞬間 に始まりcover 50% → 要素が画面の 50% くらいを覆ったあたり で終わる
その間で fadeZoomIn(フェード+ズーム 0%→100% )のアニメーションを進めます。
この2行のみで、これまでjavascriptで計算していた、フェードインのアニメーションがCSSだけで実現できるようになります。
※ なお、この2行がない場合は、自動で1秒間でズームフェードインとなります。
view 系タイムライン「区間ラベル」について
- entry 範囲
要素が「画面に入り始めてから、完全に画面内に収まるまで」の短い区間 - cover 範囲
要素が「画面に 1px でも入っているあいだ全部」の区間(入ってくる途中+完全に入っている+出ていく途中をまとめたイメージ)
これにより、animation-range: entry 0% cover 50%; は上記で説明した、
要素が画面に入り始めた瞬間(entry 0%)から、
要素が画面の 50% くらいを覆ったところ(cover 50%)までの間に、
@keyframesで定義したアニメーション fadeZoomIn を進める。
ということになります。
DEMOの注意事項
DEMOで気付いたかもしれませんが、entry 0% としているのに、最初に画像が出てきた時点で、すでに少し画像が拡大されて表示されています。
これは、Scroll-driven Animations の仕様上、「見た目の縮小や移動した状態」ではなく、本来のレイアウト上のボックスを基準 に、「画面に入ったかどうか」を判定しているためです。
transform() や scale()は無視され、本来のレイアウト上のボックスが画面に表示された時点で(画像が画面に出てくる前に)拡大アニメーションが始まっています。
そのため、縮小されている画像が画面に入ってからスタートさせるには、entry 50% など、少し先のポイントに開始位置をずらして調整します。
@supports (animation-timeline: view(block)) {
.fade-zoom {
animation-timeline: view(block);
animation-range: entry 50% cover 50%;
}
}
このanimation-rangeにはentryやcover以外にもオプションがあります。
- contain 範囲
要素が「完全に画面内に収まっている間」だけの区間
- exit 範囲
要素が「画面から出始めてから、最後の 1px が消えるまで」の区間
- entry-crossing 範囲
要素が「画面に入り始めてから、ある程度通過し終わるまで」の区間(entry を少し広めに取ったイメージ)
- exit-crossing 範囲
要素が「画面から出始めてから、ある程度抜けきるまで」の区間(exit を少し広めに取ったイメージ)
正直、entry-crossing とか exit-crossing とかまで出てくると違いがよく分からないですね。。
以下のRangeが確認できるツールが提供されていますので、animation-rangeを変更しながら確認すると理解が深まります。
View Progress Timeline: Ranges and Animation Progress Visualizer
scroll-timeline / view-timeline の関連プロパティについて
ここまでのサンプルでは animation-timeline: scroll(root block); のように、
アニメーションごとに直接 scroll() や view(block) を書いていました。
- どのスクロールコンテナをタイムラインの元にするか
- 縦スクロール / 横スクロールのどちらに連動させるか
- 画面の内側・外側どの辺りを 0% / 100% にするか
などをきちんとプロパティを指定したうえで、名前を付けて利用するという書き方もあります。
関連プロパティ
scroll-timeline系の代表的なプロパティは次の 3 つです。
scroll-timeline-name… タイムラインの名前を付ける(--から始める必要があります)scroll-timeline-axis… block(縦) / inline(横) / x(横) / y(縦) など軸を指定するscroll-timeline… 上記 2 つのショートハンド
view-timeline系は view-* となります。
view-timeline-name… 「どの要素の見え方を追いかけるか」の名前(--から始める必要があります)… block(縦) / inline(横) / x(横) / y(縦) など軸を指定するview-timeline-axisview-timeline-inset… 画面の内側・外側どの辺りを 0% / 100% にするかのオフセットview-timeline… これらのショートハンド
例えば、これまで書いていた通り、
/* 書き方A:アニメーションごとに直接 view(block) を指定 */
@supports (animation-timeline: view()) {
.feature__bg {
animation-timeline: view(block);
animation-range: entry 0% cover 100%;
}
.feature__image {
animation-timeline: view(block);
animation-range: entry 20% cover 80%;
}
}
上記のコードは下記のコードのように書くこともできます。
同じセクションの中で
「背景も画像も、同じセクションの見え方に同期させたい」
という場合に利用できます。
/* 書き方B:親にタイムライン名を生やして、子から参照する */
@supports (animation-timeline: view()) {
.feature {
/* このセクションの見え方を --feature という名前のタイムラインにする */
view-timeline-name: --feature;
view-timeline-axis: block;
}
.feature__bg {
animation-timeline: --feature; /* 親のタイムラインを使う */
animation-range: entry 0% cover 100%;
}
.feature__image {
animation-timeline: --feature; /* 同じタイムラインを共有 */
animation-range: entry 20% cover 80%;
}
}
view-timelineのview-timeline-insetについて
通常の view タイムラインは、「画面の上下」がそのまま 0% / 100% の枠になります。
view-timeline-insetを指定することで、この「画面の上下」を 内側(or 外側)にずらす ためのプロパティです。
- 正の値 → その分だけ 内側 へ(0%/100% の帯を“中に”寄せる)
- 負の値 → その分だけ 外側 へ(画面外も含めて 0%/100% を決める)
「画面の真ん中あたりに来たときからアニメーションしたい」
「画面の外にいるうちからゆっくり始めたい」
といったときに使います。
例えばview-timeline-inset: 20% 20%;
とすると、画面の上下 20% 分を除いた「中央 60% の帯」が 0%〜100% の範囲として扱われます。
「画面の端っこでは動かさず、真ん中あたりに来たときだけアニメーションしたい」ときに利用できます。
逆に、マイナスを指定して、
view-timeline-inset: -20% -20%;
とすると、画面の上下 20% 分「外側」まで含めた、画面より背の高い帯が
0%〜100% の範囲として扱われます。
この場合は、要素が画面の外にいるうちから少しずつアニメーションを始めて、
画面の外へ抜けたあともしばらくアニメーションを続けたい、といったときに利用できます。
/* 1値:上・下とも同じインセット */
// auto のときは、その方向の scroll-padding があればそれを使い、なければ 0 と同じ扱いです。
view-timeline-inset: auto;
view-timeline-inset: 200px;
view-timeline-inset: 20%;
/* 2値:上 / 下 で別々のインセット */
view-timeline-inset: 20% auto;
view-timeline-inset: auto 200px;
view-timeline-inset: 20% 200px;
非対応ブラウザの対応について
冒頭の通り、執筆時点では、新しい Chrome / Edge / Safari などは animation-timeline をサポートしていますが、Firefox はまだデフォルトでは無効になっています。
Firefoxで試す方法
デスクトップ版のFirefoxのバージョンを110以上にします。
次に上部のバーに about:config と打ち込んで、下記の画面を表示し、「危険性を承知の上で使用する」をクリックします。
※ 弊社では一切の責任は負いかねますのでご了承ください。

設定名に layout.css.scroll-driven-animations.enabled を入力して true にします。
※ Firefox Nightly(開発版) 136 以降では、このフラグが既定で有効になっています。

非対応ブラウザの対策
現状、このScroll-driven Animationを導入する場合、古いブラウザに対応するには以下の方法となります。
- まず通常の時間ベースのアニメーション(または静止)を書いておく
@supports (animation-timeline: view())付きで Scroll-driven の指定を上書きする
という構成にしておくと、安全に導入できます。
サンプルデモ
DEMOをご覧ください。
Scroll-driven Animationが実行できるブラウザではスクロールに連動してカードが右からフェードイン表示されます。
非対応ブラウザではjavascriptのIntersectionObserverを利用して画面に表示されたら自動で右からフェードインされるようになっています。
不要な箇所は除いてあります
<article class="card js-observe">
<h2>セクション 1</h2>
<p>Scroll-driven 対応ブラウザでは、スクロールに応じてスライド+フェードインします。</p>
</article>
<article class="card js-observe">
<h2>セクション 2</h2>
<p>非対応ブラウザでは、Intersection Observer で「入ってきたときに一度だけ」再生します。</p>
</article>
<article class="card static">
<h2>セクション 3</h2>
<p>非対応ブラウザでは、アニメーションさせない(静止)。</p>
</article>
.card {
max-width: 640px;
margin: 0 auto 120px;
padding: 24px 20px;
border-radius: 16px;
background: #020617;
border: 1px solid rgba(148,163,184,.6);
opacity: 0;
transform: translateX(50%);
}
/* 横からスライドイン+フェードイン */
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(50%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* 非対応ブラウザ向け:静止 */
.card.static {
opacity: 1;
transform: translateX(0);
}
/* 非対応ブラウザ向け:Intersection Observer で付くクラス */
.card.js-observe.is-visible {
animation: fadeInRight 0.6s ease-out forwards;
}
/*
* Scroll-driven Animations 対応ブラウザ向けに上書き
*/
@supports (animation-timeline: view()) {
.card {
/* Observer 用クラスは使わず、タイムライン駆動に差し替え */
animation-name: fadeInRight;
animation-duration: 1ms; /* タイムライン用にごく短く */
animation-timing-function: ease-out;
animation-fill-mode: both;
/* viewタイムラインとして使う */
animation-timeline: view(block);
/* 画面に“そこそこ入ってきてから”動き始めるように調整 */
animation-range: entry 0% cover 50%;
}
}
<script>
// Scroll-driven Animations に対応しているブラウザでは、
// Intersection Observer フォールバックは使わない
if (!CSS.supports('animation-timeline: view()')) {
const targets = document.querySelectorAll('.js-observe');
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
obs.unobserve(entry.target);
}
});
}, {
threshold: 0.4
});
targets.forEach(el => observer.observe(el));
}
</script>
DEMOコードの説明
まず非対応ブラウザ向けのCSS(静止とIntersection Observer で付くクラス )を書いています。
.card.static { ... }
.card.js-observe.is-visible { ... }
その後に、Scroll-driven Animations 対応ブラウザの場合は、CSSをスクロール連動のCSSで上書きしています。
@supports (animation-timeline: view()) { ... }
この中で animation-name: fadeInRight; を .card の全クラスにアニメーションを設定し、animation-timeline: view(block); で viewタイムラインを設定して上書きしています。
Javascriptの方でも同じように animation-timeline: view() をサポートしていなければ、Intersection Observerを実行するようにしています。
if (!CSS.supports('animation-timeline: view()')) { ... }
最後に
いかがでしょうか?
これまでスクロール連動のアニメーションは、JavaScript でスクロール量を計算して、CSS に値を流し込む実装が主流でしたが、
Scroll-driven Animations を使えば、わずか数行の CSS だけで同じような表現ができるようになってきています。
この記事で紹介したようなシンプルなものから、
下記のデモサイトにあるような「スクロールに合わせて背景やテキストが順番に動く」といったリッチな演出まで、多彩なパターンがCSSだけで実現できます。
一方で、2025年時点ではまだ
- Safari 26 より前のバージョン
- Firefox(デフォルト設定)
などでは animation-timeline / scroll() / view() がそのままでは使えません。
そのため実務では、
- 非対応ブラウザでは「アニメーションなし(静止表示)」にする
- もしくは Intersection Observer + 通常の animation で、最低限のフェードインだけフォールバックする
大まかな考え方としては、
- 「見た目のアニメーションだけで完結する部分」は CSS の
scroll()/view()に寄せる - 「カウンター更新・クラス付け替えなど、何かロジックが必要な部分」だけ Intersection Observer を併用する
といった棲み分けが、現実的な落としどころになるかと思います。
まずは影響の少ないページでは
「対応ブラウザでは少しリッチに、非対応ブラウザでは普通に見えるだけ」
という軽めの使い方から試し、慣れてきたら重要なセクションにも、
少しずつ Scroll-driven Animations を取り入れていくのが良いと思います。



