CSSで、ページを読んだ量を画面の上のバーで示す方法

​​CSS​HTML,Playwright,スクロール駆動アニメーション

ページをどこまで読んだかを画面の上のバーで示す表示は、CSS の animation-timeline: scroll() だけで作れます。バーを横に伸ばすアニメーションを用意し、その進み具合を時間ではなくページのスクロール量に結びつけます。JavaScript でスクロール量を計算する必要はありません。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開き、スクロールの位置ごとに、バーにかかっている transform の計算値を読み取りました。Firefox と Safari、スマートフォンの実機では試していません。

ページを開いた状態から、マウスのホイールで下へ3回スクロールする。スクロールするたびに右下の「スクロール 1回目/3回」の数が増え、ホイールの表示が出ている間、左下の「バーの長さ」の数字がピンクで目立つ表示になり、33%、67%、100% と増える。画面の上端の水色のバーが左から右へ伸び、3回目でフッターが見えたところで画面の幅いっぱいになる
ホイールで3回スクロールしたときの、画面の上のバーの伸び方(カーソル、ホイールの表示、下の帯は GIF 用に重ねたもの)

バーの CSS と HTML

HTML には、バーにする空の div を1つ置きます。中身のない飾りなので aria-hidden="true" を付けています。

html
<body>
  <div class="reading-progress" aria-hidden="true"></div>
  <header>サイトのヘッダー</header>
  <main>
    <!-- 本文 -->
  </main>
  <footer>サイトのフッター</footer>
</body>

CSS は次のとおりです。

css
/* ページを読んだ量を、画面の上のバーで示す */
@supports (animation-timeline: scroll()) {
  .reading-progress {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 6px;
    background: #00c8e8;
    transform-origin: 0 50%;
    /* スクロールできないページでは、タイムラインが働かず、この値のまま */
    transform: scaleX(0);
    z-index: 10;
    animation: reading-progress 1ms linear both;
    animation-timeline: scroll(root block);
  }
}

@keyframes reading-progress {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

バーは position: fixed で画面の上端に固定し、幅は画面いっぱいにしておきます。長さは width ではなく transform: scaleX() で変えます。@keyframes で scaleX(0) から scaleX(1) へ伸ばすアニメーションを作り、animation-timeline でその進み具合をスクロールに結びつけます。

transform-origin: 0 50% は、左端を基準に伸ばすための指定です。transform-origin の初期値は 50% 50%(要素の真ん中)なので、書かないとバーは真ん中から左右へ広がります。

animation の一括指定にある 1ms は再生時間ではありません。MDN にある、スクロール駆動アニメーションのタイムラインの解説によると、スクロールに結びつけたアニメーションでは animation-duration は再生時間に影響しません。ただし、Firefox は 0 でない値がないとアニメーションを適用しないため、1ms を書くのが一般的とされています。

また、MDN の animation-timeline のページにあるとおり、animation の一括指定は animation-timeline を auto に戻します。animation-timeline は animation より後に書きます。

scroll(root block) で、ページ全体のスクロール量に合わせる

scroll() は、どの要素のスクロールを使うかと、縦と横のどちらのスクロールを使うかを選ぶ関数です。root は文書のルート要素、つまりページ全体のスクロールを選びます。block は、横書きのページでは縦方向のスクロールです。

幅400px、高さ800px の画面でスクロールの位置ごとに読み取ると、バーの長さはスクロールできる量に対する割合と同じになりました。このページでスクロールできる量は 3329px です。

text
scrollY=0 最大=3329 scaleX=0.000 幅=0.0px
scrollY=832 最大=3329 scaleX=0.250 幅=100.0px
scrollY=1665 最大=3329 scaleX=0.500 幅=200.1px
scrollY=2497 最大=3329 scaleX=0.750 幅=300.0px
scrollY=3329 最大=3329 scaleX=1.000 幅=400.0px

scroll() の引数を省くと、scroll(nearest block) と同じ意味になり、いちばん近い祖先のスクロールコンテナーを使います。このページでは、animation-timeline: scroll() にしても、5つの位置で値はまったく同じでした。それでも root と書いておくと、ページ全体のスクロールを使うことがコードから読み取れます。

ページ全体のスクロール量なので、本文の下にあるフッターの分も含まれます。高さ300px のフッターがあるこのページでは、本文の末尾が画面の下端に来たとき、バーは 91.0% でした。フッターまでスクロールしきったところで 100% になります。

ポイント

バーが 100% になるのは、ページのいちばん下までスクロールしたときです。本文を読み終えた位置ではなく、ページ全体のどこまで進んだかを示します。

スクロールできないページでは、バーが端まで伸びる

ページが画面に収まり、スクロールできないときは、タイムラインが働きません。MDN の scroll() のページにも、指定した軸にスクロールバーがないと、アニメーションのタイムラインは無効になると書かれています。

検証環境で試したところ、タイムラインが無効なときは、アニメーションの値ではなく、要素にもともと指定した transform が使われました。transform を書いていないと計算値は none で、バーは画面の幅いっぱいに伸びたままになりました。上の CSS で transform: scaleX(0) を書いているのは、これを防ぐためです。書いておくと、スクロールできないページでは幅が 0 になり、バーは見えませんでした。

text
transform を書かない場合: 最大=0 scaleX=none(1) 幅=400.0px
transform: scaleX(0) を書いた場合: 最大=0 scaleX=0.000 幅=0.0px
上下2段の比較。上の「基本の transform を書かない場合」は、スクロールできないページで、画面の上端に水色のバーが端まで伸びている。下の「基本の transform: scaleX(0) を書いた場合」は、同じページでバーが見えない
スクロールできない短いページでの表示の違い

対応していないブラウザでは、バーを出さない

上の CSS は、バーのスタイルをすべて @supports の中に書いています。animation-timeline: scroll() に対応していないブラウザでは、バーはスタイルのない空の div になり、見えません。

対応していないブラウザは検証環境に用意していないので、@supports の条件をわざと成り立たないもの(animation-timeline: no-such-value)に変えて確かめました。このとき、バーの div は position が static、高さが 0px、背景は透明で、スクロールしても変わりませんでした。

@supports で囲まずに書いた場合も確かめました。animation-timeline の値を無効なもの(no-such-value)にして、この宣言が使われない状態にすると、計算値は auto になりました。MDN の animation-timeline のページによると、auto は時間で進む文書タイムラインです。このアニメーションは 1ms で終わって scaleX(1) のまま止まるので、バーはスクロールの位置にかかわらず、画面の幅いっぱいに出たままでした。

スクロールする枠の中で読んだ量を示す

利用規約の枠のように、ページの中のスクロールする枠で読んだ量を示したいときは、scroll() を引数なしで使います。バーを枠の中に置くと、いちばん近い祖先のスクロールコンテナー、つまりその枠のスクロールにバーが合います。

html
<div class="terms" tabindex="0">
  <div class="terms-progress" aria-hidden="true"></div>
  <p>第1条 …</p>
</div>
css
/* スクロールする枠の中で、読んだ量を枠の上のバーで示す */
.terms {
  height: 240px;
  overflow-y: auto;
  border: 1px solid #9aa5ab;
}

@supports (animation-timeline: scroll()) {
  .terms-progress {
    position: sticky;
    top: 0;
    height: 4px;
    background: #00c8e8;
    transform-origin: 0 50%;
    transform: scaleX(0);
    animation: reading-progress 1ms linear both;
    /* いちばん近い祖先のスクロールコンテナー(.terms)を基準にする */
    animation-timeline: scroll();
  }
}

@keyframes はページ全体のバーと同じものです。枠の中では position: fixed ではなく position: sticky と top: 0 を使い、枠をスクロールしてもバーが枠の上端に残るようにします。

「利用規約」の見出しの下に、枠で囲まれた規約の文章がある。枠は途中までスクロールされていて、第6条あたりが枠の上端に見えている。枠の上端に、枠の幅の半分くらいまで伸びた水色のバーがある
枠の中を半分までスクロールしたときの表示

枠の中をスクロールすると、バーは枠のスクロールできる量に対する割合で伸びました。バーの位置は、どのスクロール位置でも枠の上端(枠線の1px 内側)のままでした。

text
枠のscrollTop=0 枠の最大=284 scaleX=0.000
枠のscrollTop=142 枠の最大=284 scaleX=0.500
枠のscrollTop=284 枠の最大=284 scaleX=1.000

ここで scroll(root) と書くと、枠のスクロールではなくページ全体のスクロールに合ってしまいます。試したところ、枠を最後までスクロールしてもバーは 0 のままで、ページのほうを 300px スクロールしたときに 39.3% まで伸びました。

対応しているブラウザ

MDN の互換性データ(@mdn/browser-compat-data 8.1.8)では、animation-timeline と scroll() は、どちらも次のバージョンから使えます。

ブラウザ バージョン
Chrome、Edge 115
Firefox 159
Safari(macOS、iOS) 26

同じデータでは、Firefox 159 のリリース日は 2026年10月27日です。これより古いブラウザでは、@supports の中が使われないので、バーは表示されません。

​Posted by 86