スムーズなプログレスバーの作り方【JavaScript】

progress bar animationのアイキャッチ画像

progress / lerp / easing を使って、気持ちよく伸びるUIを作る

読み込み中、達成率の表示、レベルゲージ、アップロード進行状況など、プログレスバーはさまざまなUIで使われます。

ただ、単純に幅を一気に変更するだけだと、動きがカクッとして見えたり、数値の変化が急で不自然に感じたりします。

この記事では、JavaScriptでスムーズなプログレスバーを作る考え方を整理します。
特に以下の3つを中心に見ていきます。

  • progress: どこまで進んだかを表す値
  • lerp: 現在値を目標値へなめらかに近づける方法
  • easing: 動きの加速・減速をコントロールする考え方

最後には、CodePenでそのまま試せる実験も用意しています。

1. まずは「progress」とは何か

プログレスバーの基本は、とてもシンプルです。

たとえば進捗率が 0%〜100% の範囲で変化するなら、内部では次のような値を持てます。

let progress = 0;   // 現在の進捗
let target = 75;    // 目標の進捗

この progress を画面上のバーの幅に反映すれば、プログレスバーになります。

bar.style.width = `${progress}%`;

ここで重要なのは、表示値目標値を分けて考えることです。

  • target: 本当に到達したい値
  • progress: 今、画面に表示している値

この2つを分けると、UIをなめらかに制御しやすくなります。

2. いきなり値を変えると、なぜ不自然なのか

たとえば、ボタンを押した瞬間に 20% から 80% へ変えるとします。

progress = 80;
bar.style.width = `${progress}%`;

これは実装としては簡単ですが、画面上では瞬間移動のように見えます。
情報としては正しくても、体験としては少し硬い印象になります。

プログレスバーは単なる表示部品ではなく、「進んでいる感覚」を伝えるUIです。
そのため、少しずつ変化させるだけで見やすさや心地よさがかなり変わります。

3. スムーズに見せる基本は lerp

こういうときによく使うのが lerp です。
lerp は linear interpolation(線形補間) の略で、今の値を目標値へ少しずつ近づける方法です。

補間(Interpolation)の考え方はこちら
#11 Interpolation

式はこうです。

current = current + (target - current) * rate;

たとえば rate = 0.1 なら、毎フレーム「残り距離の10%だけ進む」ことになります。

progress += (target - progress) * 0.1;

この式の良いところは、次のような見た目になることです。

  • 最初は大きく進む
  • 近づくほどゆっくりになる
  • 急に止まらず、自然に収束する

つまり、機械的にカクカク動くのではなく、ふわっと目標に近づく感じが出せます。

4. lerpで動くプログレスバーの最小例

まずは最小構成のサンプルです。

<div class="progress">
  <div class="progress-fill" id="fill"></div>
</div>
<button id="btn">Increase</button>
.progress {
  width: 320px;
  height: 16px;
  background: #e5e7eb;
  border-radius: 999px;
  overflow: hidden;
}

.progress-fill {
  width: 0%;
  height: 100%;
  background: #111827;
}
const fill = document.getElementById("fill");
const btn = document.getElementById("btn");

let progress = 0;
let target = 0;

btn.addEventListener("click", () => {
  target = Math.min(target + 20, 100);
});

function loop() {
  progress += (target - progress) * 0.1;
  fill.style.width = `${progress}%`;
  requestAnimationFrame(loop);
}

loop();

これだけでも、ボタンを押すたびにバーがなめらかに伸びます。

5. ただし lerp は「いつ終わったか」が少し曖昧

lerp は便利ですが、弱点もあります。
目標値に近づき続けるものの、理論上はぴったり一致しにくいからです。

たとえば 99.999... のように近づいていくので、表示上は問題なくても、内部的には微妙にズレたままになることがあります。

そのため、実装ではある程度近づいたら丸めることが多いです。

if (Math.abs(target - progress) < 0.1) {
  progress = target;
}

これで、最後はきれいに止まります。

6. easing とは何か

easing は、時間に対する進み方を調整する考え方です。

詳しい easing の考え方はこちら
#03 easing / cubic-bezier

たとえば同じ 0% → 100% の移動でも、

  • 一定速度で進む
  • 最初ゆっくり、途中で速くなる
  • 最初速く、最後ゆっくり止まる

という違いを作れます。

よくある easing の例が easeOut です。
終わりに向かって減速するので、プログレスバーにも相性が良いです。

function easeOutCubic(t) {
  return 1 - Math.pow(1 - t, 3);
}

ここでの t は 0〜1 の時間の進行率です。

  • t = 0 で開始
  • t = 1 で終了

この t を easing 関数に通すことで、単なる直線的な動きではない、自然な変化が作れます。

7. easing で動くプログレスバーの考え方

easing を使うときは、次のように考えると分かりやすいです。

  • startValue: 開始時の値
  • endValue: 目標値
  • duration: 何ミリ秒で到達するか
  • elapsed: 経過時間

そして、経過率を求めます。

const t = Math.min(elapsed / duration, 1);

これを easing に通します。

const eased = easeOutCubic(t);

最後に、開始値から終了値の間を補間します。

const value = startValue + (endValue - startValue) * eased;

この方法は、何ミリ秒で終わるかを明確に決めたいときに向いています。

8. lerp と easing の使い分け

ここが少し混乱しやすいポイントです。

lerp が向いている場面

  • 値が頻繁に更新される
  • 目標値に追従するUIを作りたい
  • スムーズに近づく見た目がほしい

例:

  • HPバー
  • 音量スライダー表示
  • スクロール連動UI
  • リアルタイムに変わるメーター

easing が向いている場面

  • 開始と終了がはっきりしている
  • 何秒で完了するか決めたい
  • 演出的な気持ちよさを出したい

例:

  • ローディング完了演出
  • ボタン押下で進捗が増えるアニメーション
  • レベルアップ演出
  • ステップ進行UI

ざっくり言うと、

  • 追従型なら lerp
  • 演出型なら easing

と考えると整理しやすいです。

9. 実務では「進捗の真値」と「表示の見た目」を分ける

ここは大事です。

たとえばファイルアップロードの本当の進捗が 63% だったとしても、表示側は少し遅れて 58% → 60% → 62% → 63% と追いかけても構いません。

つまり、

  • 真値: 実際の進捗
  • 表示値: ユーザーに見せるアニメーション用の値

を分けると、見た目を調整しやすくなります。

let actualProgress = 63;
let displayProgress = 58;

displayProgress += (actualProgress - displayProgress) * 0.12;

この考え方を持っておくと、見た目がかなり整います。

10. 数字表示も一緒に滑らかにすると完成度が上がる

プログレスバーだけでなく、数値ラベルも同じ表示値を使うと統一感が出ます。

label.textContent = `${Math.round(progress)}%`;
fill.style.width = `${progress}%`;

こうすると、

  • バーだけ先に進む
  • 数字だけ急に変わる

といったズレが起きにくくなります。

UIでは、こうした小さな一致感が見た目の品質につながります。

11. 100% に見えても止まらない問題に注意

プログレスバーでは、見た目が 100% に見えていても内部値が 99.6 などのまま、いつまでも更新されることがあります。

これを防ぐには、終端処理を入れます。

if (Math.abs(target - progress) < 0.1) {
  progress = target;
}

さらに、CSS幅に反映するときも丸めておくと安定しやすいです。

fill.style.width = `${progress.toFixed(2)}%`;

12. プログレスバーを気持ちよく見せる小さな工夫

スムーズなプログレスバーは、計算式だけでなく見た目も大事です。

たとえば次のような工夫が有効です。

角を丸くする

border-radius: 999px;

はみ出しを隠す

overflow: hidden;

バーの高さを細めにする

細すぎると見えづらいですが、やや細めの方が洗練されて見えやすいです。

数字表示を近くに置く

バーと数値が離れすぎると、進捗の対応が分かりにくくなります。

13. 実験:プログレスバーの動きを比較する

以下の実験では、3つのモードを比較できます。

  • Jump: 目標値に一気に切り替える
  • Lerp: 目標値に少しずつ近づく
  • Ease: 一定時間で気持ちよく到達する

同じ進捗変更でも、見え方がかなり違うはずです。

特に見てほしいのは次の点です。

  • Jump は情報は正しいが、動きが硬い
  • Lerp は追従が自然で、柔らかい
  • Ease は演出として分かりやすく、完了感がある

観察ポイント

実際に触りながら、次を見てみてください。

1. どのモードが一番「自然」に見えるか

単純な正しさだけでなく、UIとして心地よいかを見ます。

2. 値が大きく変わったときの印象

15% → 90% のような大きな変化で差が分かりやすいです。

3. 細かく連続で押したときの追従感

頻繁に目標値が変わると、lerp の良さが見えやすくなります。

14. 今回のまとめ

スムーズなプログレスバーを作るときは、まず目標値表示値を分けて考えるのが基本です。

そのうえで、

  • 値に自然に追従したいなら lerp
  • 開始から終了までの演出を作りたいなら easing

という使い分けをすると整理しやすくなります。

プログレスバーは小さなUIですが、こうした細部のなめらかさが全体の品質に直結します。
読み込み表示、ゲージ、達成率、レベルアップ演出などにもそのまま応用できるので、ぜひ一度手元で試してみてください。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA