scroll() — スクロール連動

スクロールコンテナのスクロール量をタイムラインにする。 ページ進捗バー、横スクロールギャラリー、章リーディング進捗に最適。

構文

animation-timeline: scroll(<scroller> <axis>);

/* <scroller>: nearest | root | self */
/* <axis>:    block | inline | x | y */

例 1: ページ全体の進捗バー

<div class="progress"></div>
.progress {
  position: fixed; top: 0; left: 0;
  width: 100%; height: 4px;
  background: orange;
  transform-origin: left;
  transform: scaleX(0);

  animation: grow linear both;
  animation-timeline: scroll(root);
}

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

例 2: 横スクロールでカードがフェード

<div class="gallery">
  <div class="card">...</div>
  <div class="card">...</div>
  ...
</div>
.gallery {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  gap: 16px;
}

.card {
  flex: 0 0 80%;
  scroll-snap-align: start;
  animation: highlight linear;
  animation-timeline: scroll(nearest inline);  /* gallery の横スクロール */
}

@keyframes highlight {
  from { opacity: 0.5; transform: scale(0.95); }
  to   { opacity: 1;   transform: scale(1); }
}

例 3: 階層を跨ぐ scroll(root)

/* どこに配置されている要素でも、ページのスクロールを使う */
.shared-progress {
  animation: spin linear infinite;
  animation-timeline: scroll(root);
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

scroll(self) で自要素のスクロール

.scrollbox {
  height: 300px;
  overflow-y: auto;

  animation: shadow linear;
  animation-timeline: scroll(self);
}
@keyframes shadow {
  from { box-shadow: 0 0 0 transparent; }
  to   { box-shadow: 0 8px 16px rgba(0,0,0,.2); }
}

scroll() の進行度

進行度 = (現在のスクロール位置) / (最大スクロール位置)

- 上端: 0
- 下端: 1
- 真ん中: 0.5
      

animation-range で範囲を絞る

/* スクロールの 20%〜80% の間でアニメ */
animation-range: 20% 80%;

%, vh, vw, px の指定

animation-range: 100px 500px;        /* 絶対 px */
animation-range: 0vh 50vh;           /* ビューポート */
animation-range: 0% 100%;            /* スクロール全体 */

典型レシピ集

1. リーディング進捗

.reading-bar {
  position: fixed; top: 0; left: 0;
  width: 100%; height: 3px;
  background: linear-gradient(to right, orange, red);
  transform-origin: left;
  transform: scaleX(0);
  animation: read linear both;
  animation-timeline: scroll(root);
}
@keyframes read { to { transform: scaleX(1); } }

2. ヘッダーが小さくなる

.header {
  height: 80px;
  animation: shrink linear both;
  animation-timeline: scroll(root);
  animation-range: 0 200px;
}
@keyframes shrink {
  to { height: 50px; backdrop-filter: blur(8px); }
}

3. 文字色がスクロールで変わる

h1 {
  animation: color-shift linear both;
  animation-timeline: scroll(root);
}
@keyframes color-shift {
  0%   { color: #111; }
  50%  { color: #c33; }
  100% { color: #06f; }
}

4. 背景に円形の進捗

body {
  background: conic-gradient(orange 0%, transparent 0%);
  animation: ring linear both;
  animation-timeline: scroll(root);
}
@keyframes ring {
  to { background: conic-gradient(orange 100%, transparent 0%); }
}

5. 横スクロールに沿った変形

.scroller {
  display: flex;
  overflow-x: auto;
  width: 100%;
}
.item {
  flex: 0 0 100%;
  animation: tilt linear both;
  animation-timeline: scroll(nearest inline);
}
@keyframes tilt {
  0%   { transform: rotate(-3deg); }
  100% { transform: rotate(3deg); }
}

scroll-snap との相性

scroll-snap で要素単位にスナップさせると、視覚的に切れ目がはっきりする。
animation-timeline と組合せて1 ページずつ進む演出が作れる。

失敗パターン

症状対処
動かないscroll() 対象が「実際にスクロール可能」か
scroll(root) なのに動かないhtml / body に overflow:hidden が無いか
横スクロール認識しないscroll(nearest inline) を明示
カクつくtransform / opacity 以外をアニメしている
iOS で挙動が変Safari の対応状況確認 / polyfill

パフォーマンス

最初に触るレシピ

scroll(root) で進捗バー」が一番感動しやすい。10 行で書けて、JS のスクロールリスナを完全に置き換えられる。