【CSS】Scroll-driven Animations の使い方を分かりやすく解説

これまでの記事では、CSSやJavaScriptを使ってアニメーションの実装方法を紹介しました。

@keyframesanimationtransitionIntersection 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.enabledtrue にすると試せる段階
      (参考)

「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() は「スクロール量」でアニメーションを動かします。

まずはデモをご覧下さい。下にスクロールすると、画面上部のプログレスバーがスクロールする分、伸びていきます。

コードを開くを閉じる
HTML
<body>
<div class="scroll-progress"></div>
<main class="content">
    <h1>Scroll-driven Animations のテスト</h1>
    <p>下にスクロールしてみてください。</p>
    <p>上部のプログレスバーがスクロールする分伸びているのがわかります。</p>
</main>
</body>
CSS
/* スクロールする高さを確保 */
.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%の大きさになって動きが終了します。

コードを開くを閉じる
HTML
<div class="spacer"></div>
<section class="fade-zoom">
    <img class="fade-zoom__img" src="valmon.png">
</section>
CSS
.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% など、少し先のポイントに開始位置をずらして調整します。

CSS
@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

豊富なオリジナルブロックでLPをかんたんに作成できるLP Creator

LPをかんたんに作成できるLP Creator

デザイン・機能・SEO・収益化にこだわったメディア運営者向け「STREETIST」

デザイン・機能・速度・SEO・収益化にこだわった、ブロガー・メディア運営者向けのデザインテーマ STREETIST

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 … 「どの要素の見え方を追いかけるか」の名前(--から始める必要があります)
  • view-timeline-axis … block(縦) / inline(横) / x(横) / y(縦) など軸を指定する
  • view-timeline-inset … 画面の内側・外側どの辺りを 0% / 100% にするかのオフセット
  • view-timeline … これらのショートハンド

例えば、これまで書いていた通り、

CSS
/* 書き方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%;
    }
}

上記のコードは下記のコードのように書くこともできます。

同じセクションの中で
「背景も画像も、同じセクションの見え方に同期させたい」
という場合に利用できます。

CSS
/* 書き方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% の範囲として扱われます。

この場合は、要素が画面の外にいるうちから少しずつアニメーションを始めて、
画面の外へ抜けたあともしばらくアニメーションを続けたい、といったときに利用できます。

CSS
/* 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を導入する場合、古いブラウザに対応するには以下の方法となります。

  1. まず通常の時間ベースのアニメーション(または静止)を書いておく
  2. @supports (animation-timeline: view()) 付きで Scroll-driven の指定を上書きする

という構成にしておくと、安全に導入できます。

サンプルデモ

DEMOをご覧ください。

Scroll-driven Animationが実行できるブラウザではスクロールに連動してカードが右からフェードイン表示されます。

非対応ブラウザではjavascriptのIntersectionObserverを利用して画面に表示されたら自動で右からフェードインされるようになっています。

コードを開くを閉じる

不要な箇所は除いてあります

HTML
<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>
CSS
.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%;
    }
}
javascript
<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だけで実現できます。

Scroll-driven Animations

一方で、2025年時点ではまだ

  • Safari 26 より前のバージョン
  • Firefox(デフォルト設定)

などでは animation-timeline / scroll() / view() がそのままでは使えません。
そのため実務では、

  • 非対応ブラウザでは「アニメーションなし(静止表示)」にする
  • もしくは Intersection Observer + 通常の animation で、最低限のフェードインだけフォールバックする

大まかな考え方としては、

  • 「見た目のアニメーションだけで完結する部分」は CSS の scroll() / view() に寄せる
  • 「カウンター更新・クラス付け替えなど、何かロジックが必要な部分」だけ Intersection Observer を併用する

といった棲み分けが、現実的な落としどころになるかと思います。

まずは影響の少ないページでは
「対応ブラウザでは少しリッチに、非対応ブラウザでは普通に見えるだけ」
という軽めの使い方から試し、慣れてきたら重要なセクションにも、
少しずつ Scroll-driven Animations を取り入れていくのが良いと思います。