沿革セクションに使えるスクロール連動プログレスバーを実装する
ページ全体あるいは沿革セクションなどの特定のブロックで、スクロールに連動してラインが伸びる演出を時々見かけます。
今回はJavaScriptとCSS変数を組み合わせたシンプルなプログレスバーの実装方法を解説します。JavaScriptとCSSの役割を分離することで、色や太さなどの見た目の調整をCSS側だけで完結できるシンプルな構成になっています。
実装例
早速ですが以下が実装例です。
スクロール量に応じてページ上部のプログレスバーが伸びていることが確認できます。
コーポレートサイトやLPなどで時々見かける実装で、現在ページ内のどの位置にいるかを視覚的に確認でき、UX向上が期待されます。今回はシンプルなバーで実装しましたが、円形のラインなどにするとより記憶に残る印象です。
コードの解説
JavaScriptで進捗率を取得する
処理の流れはシンプルで、スクロール時やリサイズ時に呼び出すupdateProgress関数の中で現在の進捗率(0~100)を取得しCSS変数へ代入しています。各行の処理はコメントにて記載しています。
const updateProgress = () => { // 現在のスクロール位置(ページの先頭から何pxスクロールしたか) const scrollTop = window.scrollY; // ページ全体の高さ(見えていない部分も含む総高さ) const scrollHeight = root.scrollHeight; // ビューポートの高さ(現在画面に表示されている領域の高さ) const viewportHeight = window.innerHeight;
// 実際にスクロール可能な最大距離を計算 const maxScroll = scrollHeight - viewportHeight;
// スクロールの進捗率を計算(0〜100%にクランプ) const progress = maxScroll > 0 ? Math.min(Math.max((scrollTop / maxScroll) * 100, 0), 100) : 0; // CSS変数へ進捗率を代入 root.style.setProperty("--scroll-progress", progress);
ticking = false;};maxScrollを基準にすることで、ページの高さに関わらずスクロール完了時に必ず100%になるよう正規化しています。またMath.minとMath.maxでクランプしているのは、(今回は使用していませんが)慣性スクロールなどで範囲外の値が入り込むのを防ぐためです。
htmlに設定した--scroll-progressがスクロールに応じて0~100の間で変化していることが確認できます。
スクロールイベントは高頻度で発火するため、requestAnimationFrameでupdateProgressの実行をブラウザの描画タイミングに合わせています。これにより処理の呼び出しを間引き、不要な再計算を避けてスクロール処理を軽量に保っています。
CSS変数をwidthに反映する
CSSではJavaScriptで取得した進捗率を、プログレスバーの要素である.progress-barのwidthプロパティに参照させています。これによりバーの横幅が動的に変化するようなります。
.progress-bar { /* 他は省略 */ width: calc(var(--scroll-progress) * 1%);}ポイントとしては、そのまま--scroll-progressを代入するだけでは単位がなく機能しないので、calcを用いて%単位に値を変換しています。
この実装のメリット
今回の実装では、JavaScriptはスクロール量の取得だけを担当し、見た目の制御はCSSに任せています。
こうすることでデザイン調整をCSS側だけで完結できるため、『バーの色を変えたい』『太さを調整したい』といった修正依頼が来ても、JavaScriptに触れずにCSSだけで対応可能です。
またスクロールの進捗度に連動した見た目の変化が複数存在するような場合にも、HTMLとCSSの追加だけで対応することができます。
応用例:ラインが伸びる沿革セクション
応用例として、沿革セクションの縦のラインがスクロールに応じて伸びるアニメーションを作成してみました。こちらもコーポレートサイトなどで時々見かける演出ではないでしょうか。
会社が歩んできた歴史を視覚的に表現することができ、ページに動きと奥行きを加えられる演出だと思います。
実装のポイント
基本的には先ほどと同じ仕組みで、data属性でアニメーションの開始・終了位置を要素ごとに指定できるようにAIが拡張しています。
const updateProgress = () => { // 現在のスクロール位置 const scrollTop = window.scrollY; // ビューポートの高さ const viewportHeight = window.innerHeight;
// 対象要素ごとに処理 elms.forEach((el) => { // 要素のビューポート基準の位置・サイズを取得 const rect = el.getBoundingClientRect(); // ドキュメント基準の要素上端位置 const elTop = rect.top + scrollTop;
// data属性から開始・終了位置を取得(% → 0〜1へ変換) const startElement = parseFloat(el.dataset.startElement ?? 0) / 100; const startViewport = parseFloat(el.dataset.startViewport ?? 100) / 100; const endElement = parseFloat(el.dataset.endElement ?? 100) / 100; const endViewport = parseFloat(el.dataset.endViewport ?? 0) / 100;
// アニメーション開始スクロール位置 const startScroll = elTop + rect.height * startElement - viewportHeight * startViewport; // アニメーション終了スクロール位置 const endScroll = elTop + rect.height * endElement - viewportHeight * endViewport; // 開始〜終了までのスクロール距離 const range = endScroll - startScroll;
// スクロール進捗率(0〜100%にクランプ) const progress = range > 0 ? Math.min(Math.max(((scrollTop - startScroll) / range) * 100, 0), 100) : 0; // CSS変数へ進捗率を反映 el.style.setProperty("--scroll-progress", progress); });
ticking = false;};各data属性の意味は次の通りです。
| data属性 | 意味 | デフォルト値 |
|---|---|---|
| data-start-element | 開始:要素のどの位置か(0=天端, 100=底端) | 0 |
| data-start-viewport | 開始:ビューポートのどの位置か(0=上端, 100=下端) | 100 |
| data-end-element | 終了:要素のどの位置か | 100 |
| data-end-viewport | 終了:ビューポートのどの位置か | 0 |
要素ごとに開始・終了位置を細かく指定できるため、沿革セクションの長さやデザインが変わった際にも、JavaScriptを修正せずにHTML側の数値調整だけで演出をコントロールできます。
今回はアニメーションの基準となるビューポートの位置を下記のように設定しました。
<ul class="p-history__list js-scroll-progress" data-start-viewport="80" data-end-viewport="80">data-start-viewport="80"はビューポートの上端から80%の位置、つまり画面下部から20%の位置に要素の天端が差し掛かった時点でアニメーションが始まることを意味します。
data-end-viewport="80"はビューポートの上端から80%の位置、つまり画面下部から20%の位置に要素の底端が差し掛かった時点でアニメーションが終了することを意味します。
まとめ
今回はピュアなJavaScriptとCSS変数を用いてプログレスバーを実装してみました。JavaScriptとCSSの役割を明確に分けることで、カスタマイズや再利用のしやすいコードとなりました。
今回は使用しませんでしたが、GSAPを導入しているプロジェクトであれば、ScrollTriggerのonUpdateコールバック内でself.progressを利用する方法も選択肢の一つです。
