開発情報・ナレッジ

投稿者: 株式会社ゴンドラ 2025年8月26日 (火)

フォームの入力状況を確認できる進捗バーを導入する方法

本記事では、フォーム入力の進捗度を可視化するプログレスバーの実装方法を紹介します。

プログレスバーを設置するだけで、入力の現在地がひと目で分かり、離脱抑止と完了率向上に寄与します。軽量で既存フォームに簡単に組み込み可能です。

ポイント: 視覚的な進捗提示は入力ストレスの低減に有効

導入効果
- 完了率の見える化でモチベーション維持
- 入力体験の向上(スムーズなアニメーション)
- ページ上部に固定され常に視認可能
- 軽量・依存なしで既存フォームにすぐ追加

できること(メリット詳細)

デモサイト

デモサイトで動作をご確認頂けます。 テキストを入力してみたり、ラジオボタン、チェックボックス等を選択したりして動作をご確認ください。

  • 離脱抑止: 目標達成までの距離が明確になり完了率向上が期待
  • UX向上: スムーズな変化と演出で入力体験がリッチに
  • 常時可視: 上部固定により入力中も進捗を見失わない
  • 導入容易: 既存フォームにHTML/CSS/JSを追加するだけ
  • ブランド適合: 色や動きを簡単にブランドトーンへ調整可能

コードの説明と実装方法

プログレスバーのCSS
/* CSS ここから */
:root {
  /* 背景色(白) */
  --background: #FFFFFF;
  /* 枠線色(薄いグレー) */
  --border: #B3B3B3;
  /* プログレスバーのグラデーション色(明るい側/濃い側) */
  --gradient-light: #00a1eb;
  --gradient-dark:  #00ffd9;
}

.progress-bar-area {
  padding: 10px 18px 6px 18px;
  background-color: var(--background);
  border-bottom: 2px solid var(--border);
}

.progress-bar-container {

  background-color: var(--background);
  overflow: hidden;
  margin: 8px auto;
  border: 1px solid var(--border);
  border-radius: 10px;
}

.progress-bar {
  height: 10px;
  width: 0%;
  /* グラデーションの色は :root の --gradient-light / --gradient-dark を変更 */
  background: linear-gradient(
    to right,
    var(--gradient-light) 0%,
    var(--gradient-dark) 25%,
    var(--gradient-light) 50%,
    var(--gradient-dark) 75%,
    var(--gradient-light) 100%
  );
  background-size: 200% 100%;
  background-position: 0% 0%;
  /* 下記のアニメーション速度は JS 側でも上書きできます */
  animation: gradient-move 8s linear infinite;
  transition: width 1s ease; /* 伸縮速度は JS 側でも上書きできます */
  position: relative;
  overflow: visible;
  border-radius: 10px;
}

@keyframes gradient-move {
  0% {
    background-position: 0% 0%;
  }
  100% {
    background-position: 100% 0%;
  }
}

.fixed-progress {
  position: fixed;
  top: 0;
  z-index: 1000;
  width: calc(100% - 36px);
}

/* CSS ここまで */
実装方法
CSSはページ内の
<style>
タグ内に記載してください。
PC版とスマートフォン版でCSSを分ける必要はありません。
ただし、スマートフォン版のCSSを追加する場合は、PC版のCSSよりも下に記載してください。
プログレスバーのHTML
<div class="progress-bar-area">
   <span>進捗度: </span>
   <span id="progress-text"></span>
   <div class="progress-bar-container">
   <div class="progress-bar" id="progress-bar"></div>
   </div>
</div>
実装方法
このHTMLを使用して進捗度バーを表示します。
<body class="body">
タグと
<center>
タグの間に記載してください
プログレスバーを制御するJavascript
<script>
(function () {
  'use strict';
  
  const SETTINGS = {
    // バーの幅が変わるトランジション時間(ミリ秒)
    TRANSITION_MS: 1000,

    // 背景グラデーション(CSS keyframes: gradient-move)の再生時間(ミリ秒)
    GRADIENT_ANIM_MS: 8000,

    // 伸縮方向に応じて付与するリップル演出の長さ(ミリ秒)
    RIPPLE_MS: 700,

    // スクロールがこの値以上で固定クラスを付与
    FIXED_SCROLL_THRESHOLD: 1,

    // 固定表示する際の上部オフセット(px)。必要に応じて変更
    FIXED_TOP_PX: 0,
  };

  // =============================================================
  // 定数
  // =============================================================
  const INPUT_SELECTOR = 'input, textarea, select';
  const PROGRESS_BAR_ID = 'progress-bar';
  const PROGRESS_TEXT_ID = 'progress-text';
  const PROGRESS_CONTAINER_SELECTOR = '.progress-bar-area';

  // -------------------------------------------------------------
  // ユーティリティ
  // -------------------------------------------------------------

  function getGroupName(name) {
    return name.split(':')[0];
  }

  function isFieldFilled(element) {
    if (element.type === 'text' || element.type === 'password' || element.tagName === 'TEXTAREA') {
      return element.value.trim() !== '';
    }
    if (element.tagName === 'SELECT') {
      return element.value !== '';
    }
    return false;
  }

  function collectVisibleFormElements() {
    return Array.from(document.querySelectorAll(INPUT_SELECTOR)).filter(el =>
      el.offsetParent !== null &&
      el.type !== 'hidden' &&
      el.type !== 'submit' &&
      el.type !== 'button' &&
      el.type !== 'reset'
    );
  }

  function groupFieldsByName(elements) {
    const groups = {};
    elements.forEach(el => {
      const name = el.getAttribute('name');
      if (!name) return;
      if (el.type === 'hidden') return;
      const group = getGroupName(name);
      if (!groups[group]) groups[group] = [];
      groups[group].push(el);
    });
    return groups;
  }

  function computeProgress(groups) {
    let completedGroups = 0;
    let totalGroups = 0;

    Object.values(groups).forEach(groupFields => {
      const visibleFields = groupFields.filter(el => el.type !== 'hidden');
      if (visibleFields.length === 0) return;
      totalGroups++;

      const isRadioOrCheckbox = visibleFields.some(el => el.type === 'radio' || el.type === 'checkbox');
      if (isRadioOrCheckbox) {
        if (visibleFields.some(el => el.checked)) {
          completedGroups++;
        }
      } else {
        if (visibleFields.every(isFieldFilled)) {
          completedGroups++;
        }
      }
    });

    // グループが0件のときは 0% を返す(初期表示でNaN%にならないように)
    const percent = totalGroups > 0 ? (completedGroups / totalGroups) * 100 : 0;
    return { completedGroups, totalGroups, percent };
  }

  function renderProgress(progress) {
    const progressBar = document.getElementById(PROGRESS_BAR_ID);
    const progressText = document.getElementById(PROGRESS_TEXT_ID);
    if (!progressBar || !progressText) return;

    progressBar.style.width = progress.percent + '%';
    progressText.innerText = Math.round(progress.percent) + '%';

    console.log(progress.completedGroups);
    console.log(progress.totalGroups);
  }

  // -------------------------------------------------------------
  // 付随機能(固定表示/リップルアニメーション)
  // -------------------------------------------------------------

  function setupFixedProgressOnScroll() {
    window.addEventListener('scroll', function () {
      const progressBarContainer = document.querySelector(PROGRESS_CONTAINER_SELECTOR);
      if (!progressBarContainer) return;

      const scrollTop = window.scrollY;
      const shouldFix = scrollTop >= SETTINGS.FIXED_SCROLL_THRESHOLD;

      if (shouldFix) {
        progressBarContainer.classList.add('fixed-progress');
        // 固定時の上端位置を設定(CSSのtopを上書き)
        progressBarContainer.style.top = SETTINGS.FIXED_TOP_PX + 'px';
      } else {
        progressBarContainer.classList.remove('fixed-progress');
        progressBarContainer.style.top = '';
      }
    });
  }

  function setupRippleAnimation() {
    const progressBar = document.getElementById(PROGRESS_BAR_ID);
    if (!progressBar) return;

    // 伸縮速度(transition)を設定
    progressBar.style.transition = 'width ' + SETTINGS.TRANSITION_MS + 'ms ease';

    // グラデーションの移動速度(CSS変数で渡せないため style.animationDuration を直接設定)
    // 既存CSS: animation: gradient-move 8s linear infinite;
    progressBar.style.animationDuration = SETTINGS.GRADIENT_ANIM_MS + 'ms';

    let rippleTimeout;
    let previousWidth = parseFloat(progressBar.style.width) || 0;

    const observer = new MutationObserver(mutations => {
      mutations.forEach(mutation => {
        if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
          const newWidth = parseFloat(progressBar.style.width) || 0;
          if (newWidth === previousWidth) return;

          const rippleClass = newWidth > previousWidth ? 'rippling-right' : 'rippling-left';

          progressBar.classList.remove('rippling-right', 'rippling-left');
          void progressBar.offsetWidth; // Reflow
          progressBar.classList.add(rippleClass);

          clearTimeout(rippleTimeout);
          rippleTimeout = setTimeout(() => {
            progressBar.classList.remove(rippleClass);
          }, SETTINGS.RIPPLE_MS);

          previousWidth = newWidth;
        }
      });
    });

    observer.observe(progressBar, { attributes: true });
  }

  // -------------------------------------------------------------
  // 初期化
  // -------------------------------------------------------------

  function updateProgressBar() {
    const elements = collectVisibleFormElements();
    const groups = groupFieldsByName(elements);
    const progress = computeProgress(groups);
    renderProgress(progress);
  }

  function init() {
    document.querySelectorAll(INPUT_SELECTOR).forEach(element => {
      element.addEventListener('input', updateProgressBar);
    });

    setupFixedProgressOnScroll();
    setupRippleAnimation();

    updateProgressBar();
  }

  window.addEventListener('load', init);
})();
</script>
実装方法
<body class="body">
タグ内の一番下に設置してください。

カスタマイズ(色・動きの変更ガイド)

ここでは、CSSと JavaScript の「先頭の変数」だけを変えることで、 見た目の色やアニメーションの速さ、固定位置を簡単に調整する方法を説明します。

ポイント
「先頭の変数だけ」を変える設計なので、プログラムの知識がなくても安心して調整できます。
1. 色の変更

次の4つの項目を変更すると、見た目の色が一括で切り替わります。

:root {
  --background: #FFFFFF;      /* 背景の色(白) */
  --border: #B3B3B3;          /* 枠線の色(薄いグレー) */
  --gradient-light: #00a1eb;
  --gradient-dark:  #00ffd9;
}
  • バーの色を変えたい: --gradient-light--gradient-dark をお好みの2色に。
  • 単色にしたい: バーのCSS内の background: linear-gradient(...)background-color: 好きな色; に変更。
  • 枠線の色: --border を変更。
  • 背景の色: --background を変更。

色の書き方は #RRGGBB(例: #16A34A)や、色名(例: red)でもOKです。

2. 動き(速さ・固定位置)の変更

次の項目を変えるだけで、アニメーションの速さや固定位置を調整できます。

const SETTINGS = {
  TRANSITION_MS: 1000,   // バーが伸び縮みする速さ(ミリ秒)
  GRADIENT_ANIM_MS: 8000,// 背景の流れる速さ(ミリ秒)
  RIPPLE_MS: 700,        // 伸縮時の演出の長さ(ミリ秒)
  FIXED_SCROLL_THRESHOLD: 1, // 何pxスクロールで上部固定にするか
  FIXED_TOP_PX: 0,       // 固定するときの上からの位置(px)
};
  • 伸縮の速さ: TRANSITION_MS(数値を小さくすると速くなります)
  • 背景の流れる速さ: GRADIENT_ANIM_MS(小さいほど速い)
  • 演出の長さ: RIPPLE_MS(演出が長く続きます)
  • 固定の開始位置: FIXED_SCROLL_THRESHOLD(この値以上スクロールすると固定)
  • 固定時の位置: FIXED_TOP_PX(ヘッダーがある場合はその高さぶん加算)
3. 作業の流れ(かんたん手順)
  1. 色を変える: code3-1 のいちばん上(:root {...})の数値を変更
  2. 動きを変える: code2-1 のいちばん上(SETTINGS {...})の数値を変更
  3. SPIRALへ反映し、ブラウザで表示を確認
4. うまく変わらない時
  • ブラウザをリロードしてください。

【お知らせ】本実装の構築代行・カスタマイズを承ります

記事でご紹介した内容や、貴社の要件に合わせた柔軟なカスタマイズ開発を承っております。

SPIRALの専門チームが要件定義から実装まで一貫してサポートいたしますので、まずはお見積もりをご依頼ください。

カスタマイズの見積もりを依頼する
解決しない場合はこちら コンテンツに関しての
要望はこちら