沿革セクションに使えるスクロール連動プログレスバーを実装する

ページ全体あるいは沿革セクションなどの特定のブロックで、スクロールに連動してラインが伸びる演出を時々見かけます。

今回はJavaScriptとCSS変数を組み合わせたシンプルなプログレスバーの実装方法を解説します。JavaScriptとCSSの役割を分離することで、色や太さなどの見た目の調整をCSS側だけで完結できるシンプルな構成になっています。

実装例

早速ですが以下が実装例です。

CSS変数で実装するシンプルなプログレスバー

スクロール量に応じてページ上部のプログレスバーが伸びていることが確認できます。

コーポレートサイトやLPなどで時々見かける実装で、ユーザーが現在地を把握しやすくなり、読み進めるモチベーションにもつながります。今回はシンプルなバーで実装しましたが、円形のラインなどにするとより印象的な演出になります。

コードの解説

JavaScriptで進捗率を取得する

処理の流れはシンプルで、スクロール時やリサイズ時に呼び出すupdateProgress関数の中で現在の進捗率(0~100)を取得しCSS変数へ代入しています。各行の処理はコメントにて記載しています。

script.js
// ルート要素を取得
const root = document.documentElement;
// フラグを初期化
let ticking = false;
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.minMath.maxでクランプしているのは、(今回は使用していませんが)慣性スクロールなどで範囲外の値が入り込むのを防ぐためです。

htmlに設定した--scroll-progressがスクロールに応じて0~100の間で変化していることが確認できます。

スクロールイベントは高頻度で発火するため、requestAnimationFrameupdateProgressの実行をブラウザの描画タイミングに合わせています。

script.js
// スクロールイベントのコールバック関数
const onScroll = () => {
if (!ticking) {
requestAnimationFrame(updateProgress);
ticking = true;
}
};

これにより描画フレームごとに1回だけ実行されるよう制御し、不要な再計算を避けてスクロール処理を軽量に保っています。

CSS変数をwidthに反映する

CSSではJavaScriptで取得した進捗率を、プログレスバーの要素である.progress-barwidthプロパティに反映しています。これによりバーの横幅が動的に変化するようになります。

style.css
.progress-bar {
/* 他は省略 */
width: calc(var(--scroll-progress) * 1%);
}

ポイントとしては、そのまま--scroll-progressを代入するだけでは単位がなく機能しないので、calcを用いて%単位に値を変換しています。

この実装のメリット

今回の実装では、JavaScriptはスクロール量の取得だけを担当し、見た目の制御はCSSに任せています。

こうすることでデザイン調整をCSS側だけで完結できるため、『バーの色を変えたい』『太さを調整したい』といった修正依頼が来ても、JavaScriptに触れずにCSSだけで対応可能です。

またスクロールの進捗度に連動した見た目の変化が複数存在するような場合にも、HTMLとCSSの追加だけで対応することができます。

応用例:ラインが伸びる沿革セクション

応用例として、沿革セクションの縦のラインがスクロールに応じて伸びるアニメーションを作成してみました。こちらもコーポレートサイトなどで時々見かける演出ではないでしょうか。

スクロールに応じてラインが伸びる沿革セクション

会社が歩んできた歴史を視覚的に表現することができ、ページに動きと奥行きを加えられる演出だと思います。

実装のポイント

基本的には先ほどと同じ仕組みで、data属性でアニメーションの開始・終了位置を要素ごとに指定できるように拡張しています。

script.js
// 対象要素を取得
const elms = document.querySelectorAll(".js-scroll-progress");
// フラグを初期化
let ticking = false;
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側の数値調整だけで演出をコントロールできます。

今回はアニメーションの基準となるビューポートの位置を下記のように設定しました。

index.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%の位置に要素の下端が差し掛かった時点でアニメーションが終了することを意味します。

CSSでは取得した進捗率を、ラインを表示する擬似要素のheightプロパティに反映しています。

style.css
.p-history__list::before {
content: "";
width: var(--_line-size);
height: calc(var(--scroll-progress) * 1%); /* 進捗率に応じてラインの高さを変化 */
background-color: currentColor;
position: absolute;
top: 5px;
left: calc(calc(var(--_dot-size) / 2) - calc(var(--_line-size) / 2));
}

まとめ

今回はピュアなJavaScriptとCSS変数を用いてプログレスバーを実装してみました。JavaScriptとCSSの役割を明確に分けることで、カスタマイズや再利用のしやすいコードとなりました。

GSAPを導入しているプロジェクトであれば、ScrollTriggerのonUpdateコールバック内でself.progressを利用する方法も有効です。ScrollTriggerを使うと、開始・終了位置の指定が宣言的に書けるほか、パフォーマンス最適化やリサイズ時の再計算もライブラリ側で処理されるため、自前の制御コードを省略できそうです。

この記事を書いた人

写真:ブール 代表 西川雅人

Masato Nishikawa

ウェブ制作会社・デザイナー様向けに、コーディングやWordPress実装を承っているフリーランスです。繁忙期のサポートや、継続的な外注先をお探しでしたら、お気軽にお声がけください。

コーディングや
WordPress実装の
ご相談を承っています。

新規制作から運用フェーズの修正まで柔軟に対応します。
お見積りや実装可否の確認など、お気軽にご連絡ください。