Scroll-driven animationsでスクロールに合わせてCSSのアニメーションを進める
Scroll-driven animationsは、時間ではなくスクロールの位置や要素の見え方でCSSのアニメーションを進める機能です。Chrome 115とSafari 26に続いてFirefox 159が対応し、Baseline 2026に加わりました。scroll()とview()の2つのタイムラインと、animation-rangeで進む区間を決める書き方を整理します。
- Chrome115
- Edge115
- Firefox159
- Safari26
はじめに
読み進めた割合をバーで示したり、画面に入ってきたカードを少しずつ表示したりしたいことがあります。これまではscrollイベントやIntersectionObserverで状態を受け取り、JavaScriptからスタイルを書き換えていました。
Scroll-driven animationsは、CSSのアニメーションを時間ではなくスクロールで進める機能です。スクロールの位置で進めるscroll()と、要素の見え方で進めるview()の2つを使い分けます。Chrome 115とSafari 26に続いてFirefox 159が対応し、Baseline 2026に加わりました。
scroll()でスクロールの位置に合わせる
animation-timeline: scroll()を書くと、アニメーションはスクロールコンテナのスクロール位置で進みます。このタイムラインは、先頭にいるときが0%、末尾までスクロールしたときが100%です。
@keyframes grow {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
.progress {
position: sticky;
top: 0;
transform-origin: 0 50%;
animation: grow auto linear;
animation-timeline: scroll();
}animation-timelineは、先に書くと後のanimationショートハンドに初期値で上書きされるので、animationより後に書きます。仕様の草案ではanimationの中にscroll()も書けますが、Chrome 156、Safari 27.2、Firefox 159のどれでも宣言ごと無効になります。animationの時間に書いたautoは、タイムライン全体を使って進めるための値です。
scroll()の引数には、見るスクロールコンテナと軸を書きます。スクロールコンテナはnearest(最も近い祖先。既定値)・root(ビューポート)・self(要素自身)から、軸はblock(既定値)・inline・x・yから選びます。
animation-timeline: scroll()で、スクロールコンテナを読み進めた割合に合わせてプログレスバーを伸ばします。
段落 1:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 2:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 3:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 4:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 5:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 6:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 7:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 8:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 9:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 10:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 11:スクロールすると、上のバーが読み進めた割合だけ伸びます。
段落 12:スクロールすると、上のバーが読み進めた割合だけ伸びます。
スクロールコンテナの中身があふれずスクロールできないときは、タイムラインが働かず、キーフレームの値が当たりません。プログレスバーの例では、ページが短いと最初から伸びきった状態で表示されます。
view()で要素の見え方に合わせる
scroll()は、スクロールコンテナ全体のうちどこまでスクロールしたかを見ます。これに対してanimation-timeline: view()は、アニメーションする要素自身がスクロールコンテナの見えている領域(スクロールポート)のどこにあるかを見ます。このタイムラインは、要素が末尾側の端から見え始めたときが0%、先頭側の端から出きったときが100%です。ページのどこに置いた要素でも、スクロールポートを横切っている間だけタイムラインが進みます。
view()の引数には、scroll()と同じ軸と、見えていると判定する範囲を内側へ狭めるinsetを書けます。
@keyframes reveal {
from {
clip-path: inset(0 100% 0 0);
}
to {
clip-path: inset(0);
}
}
.card {
animation: reveal auto linear both;
animation-timeline: view();
animation-range: entry;
}animation-rangeを書かないと、アニメーションは見え始めから出きるまでをかけて進むので、スクロールポートの真ん中に来たカードはまだ左半分しか見えません。animation-rangeはアニメーションを進める区間を決めるプロパティで、上の例のようにentryを書くと、入りきった時点で全体が見えます。区間には次の4つの名前を使えます。
cover:見え始めてから出きるまでの全体(既定値のnormalと同じ)contain:要素全体がスクロールポートに収まっている間entry:入り始めてから入りきるまでexit:出始めてから出きるまで
animation-fill-modeをbothにしておくと、区間の前では最初の、後では最後のキーフレームの値が保たれます。animation-rangeもanimation-timelineと同じく、先に書くと後のanimationで上書きされるので、animationより後に書きます。こちらは仕様でもanimationの中には書けません。
animation-rangeを切り替えて進む区間を比べる
scroll-driven-animations/playgrounds/scroll-driven-animationsanimation-timeline: view()でカードを左から少しずつ表示し、animation-rangeを切り替えてカードが現れる区間を比べられます。
カードのanimation-range
カード 1
カード 2
カード 3
デモでは、カードのanimation-rangeを切り替えられます。coverではカードがスクロールポートの真ん中に来ても左半分しか見えず、entryでは入りきった時点で全体が見えます。
名前を付けて離れた要素を動かす
scroll()は祖先のスクロールコンテナを、view()は要素自身を見ます。それ以外の要素に合わせて動かしたいときは、タイムラインに名前を付けます。スクロールコンテナにはscroll-timelineで、見え方を追う要素にはview-timelineで、--から始まる名前を宣言します。動かす要素のanimation-timelineには、その名前を書きます。
仕様では、名前を文書全体から参照できます。Firefox 159は仕様どおり隣の要素からも参照できましたが、Chrome 156とSafari 27.2では宣言した要素とその子孫からしか参照できませんでした。祖先ではない要素から参照するときは、共通の祖先にtimeline-scopeで名前を書きます。
.layout {
timeline-scope: --panel;
}
.layout > .panel {
overflow-y: auto;
scroll-timeline: --panel;
}
.layout > .indicator {
animation: grow auto linear both;
animation-timeline: --panel;
}Chrome 156とSafari 27.2のtimeline-scopeは、子孫が宣言した名前を、その要素の中のどこからでも参照できるようにします。仕様のtimeline-scopeは逆に、参照できる範囲をその要素の中に限ります。どちらの動きでも、.layoutの中のインジケーターはパネルのタイムラインを参照できます。
おわりに
Scroll-driven animationsで、スクロールに合わせたアニメーションをCSSだけで書けるようになりました。スクロールの位置で進めるならscroll()、要素の見え方で進めるならview()を使い、進む区間はanimation-rangeで絞ります。Web Animations APIでも、ScrollTimelineかViewTimelineをelement.animate()のtimelineに渡せば同じことができます。