Stepper 1 — ステッパー アンケートウィザード

表示・インジケーター 初級

このコンポーネントについて

ステッパー(stepper)は、複数のステップで構成されるフローの「現在地」を視覚的に示すUIコンポーネントです。「全体で何ステップあり、今どこにいるか」が一目でわかるため、ユーザーが途中で不安や迷子感を覚えにくくなります。ステップインジケーター・ウィザードと呼ばれることもあります。

アンケート・会員登録・購入フローなど、ユーザーに複数の操作を順番にこなしてもらうシーンで広く使われます。このページでは5ステップのアンケートフォームを例に、ステッパーの実装パターンを紹介します。

  • ステップインジケーター — 全ステップ数と現在のステップ番号をドット+接続ラインで表示する。完了済みはチェックマーク、現在は強調表示
  • 多様な入力タイプ — 択一(ラジオボタン)・複数選択(チェックボックス)・テキスト入力の各ステップをカバー
  • バリデーション — 必須回答がない場合は次のステップへ進めず、エラーメッセージを表示
  • 確認画面 — 最終ステップで全回答内容をまとめて表示する

実装のポイント・注意点

ステッパーの核心は「どのステップを表示するか」の制御です。このサンプルでは、各ステップのブロックに hidden 属性を付け外しするだけでステップ切り替えを実現しています。display: none をJSで操作するより element.hidden = true / false の方が意図が明確で、CSSとの干渉も起きにくいです。

確認画面(Step 5)には、ユーザーが入力したテキストを innerHTML で表示します。このとき 必ず escapeHtml() でサニタイズしてください。サニタイズを省くと、悪意ある入力(例:<script>タグ)が実行されるXSS脆弱性につながります。サンプルコードには対策済みの escapeHtml() 関数が含まれています。

スマートフォン幅でインジケーターが窮屈になる場合は、ラベルテキストを display: none にして番号ドットだけ表示するのが定番の対処です。接続ラインはCSSの ::before 疑似要素で描画しており、right: 50%; width: 100% の組み合わせで前のステップとの中点を結ぶラインを作っています。

HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。

デモ

    アンケートにご協力ください

    このアンケートはサービス改善を目的としています。所要時間は約2分です。

    サンプルソース

    3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
    ファイル名:index.html / style.css / script.js — 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。

    <!DOCTYPE html>
    <html lang="ja">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>ステッパー サンプル</title>
      <link rel="stylesheet" href="./style.css">
    </head>
    <body>
    
    <div class="stp-root">
    
      <!-- ステップインジケーター(JSで自動生成) -->
      <ol class="stp-indicator" id="stp-indicator" aria-label="進捗"></ol>
    
      <div class="stp-body">
    
        <!-- Step 1: 説明 -->
        <div id="stp-step-1">
          <h2 class="stp-step-title">アンケートにご協力ください</h2>
          <p class="stp-step-desc">このアンケートはサービス改善を目的としています。所要時間は約2分です。</p>
          <div class="stp-actions">
            <button class="stp-btn-primary" type="button" onclick="goNext()">回答を始める</button>
          </div>
        </div>
    
        <!-- Step 2: 択一(ラジオボタン) -->
        <div id="stp-step-2" hidden>
          <h2 class="stp-step-title">Q1. あなたの職業を教えてください</h2>
          <fieldset class="stp-fieldset">
            <legend class="stp-visually-hidden">職業を選択してください</legend>
            <label class="stp-choice-label"><input type="radio" name="job" value="エンジニア"> エンジニア</label>
            <label class="stp-choice-label"><input type="radio" name="job" value="デザイナー"> デザイナー</label>
            <label class="stp-choice-label"><input type="radio" name="job" value="ディレクター"> ディレクター</label>
            <label class="stp-choice-label"><input type="radio" name="job" value="その他"> その他</label>
          </fieldset>
          <p class="stp-error" id="stp-error-2" hidden>回答を選択してください</p>
          <div class="stp-actions">
            <button class="stp-btn-secondary" type="button" onclick="goPrev()">戻る</button>
            <button class="stp-btn-primary"   type="button" onclick="goNext()">次へ</button>
          </div>
        </div>
    
        <!-- Step 3: 複数選択(チェックボックス) -->
        <div id="stp-step-3" hidden>
          <h2 class="stp-step-title">Q2. 使っているフロントエンド技術を選んでください(複数可)</h2>
          <fieldset class="stp-fieldset">
            <legend class="stp-visually-hidden">技術を選択してください</legend>
            <label class="stp-choice-label"><input type="checkbox" name="tech" value="HTML"> HTML</label>
            <label class="stp-choice-label"><input type="checkbox" name="tech" value="CSS"> CSS</label>
            <label class="stp-choice-label"><input type="checkbox" name="tech" value="JavaScript"> JavaScript</label>
            <label class="stp-choice-label"><input type="checkbox" name="tech" value="TypeScript"> TypeScript</label>
            <label class="stp-choice-label"><input type="checkbox" name="tech" value="React"> React</label>
            <label class="stp-choice-label"><input type="checkbox" name="tech" value="Vue"> Vue</label>
            <label class="stp-choice-label"><input type="checkbox" name="tech" value="その他"> その他</label>
          </fieldset>
          <p class="stp-error" id="stp-error-3" hidden>1つ以上選択してください</p>
          <div class="stp-actions">
            <button class="stp-btn-secondary" type="button" onclick="goPrev()">戻る</button>
            <button class="stp-btn-primary"   type="button" onclick="goNext()">次へ</button>
          </div>
        </div>
    
        <!-- Step 4: テキスト入力(任意) -->
        <div id="stp-step-4" hidden>
          <h2 class="stp-step-title">Q3. 改善してほしい点や要望があればお聞かせください(任意)</h2>
          <textarea class="stp-textarea" id="stp-comment" rows="4" placeholder="自由にご記入ください"></textarea>
          <div class="stp-actions">
            <button class="stp-btn-secondary" type="button" onclick="goPrev()">戻る</button>
            <button class="stp-btn-primary"   type="button" onclick="goNext()">次へ</button>
          </div>
        </div>
    
        <!-- Step 5: 完了・確認 -->
        <div id="stp-step-5" hidden>
          <div class="stp-complete-icon" aria-hidden="true">✓</div>
          <p class="stp-complete-message">ご回答ありがとうございました</p>
          <div class="stp-confirm" id="stp-confirm"></div>
          <div class="stp-actions">
            <button class="stp-btn-secondary" type="button" onclick="goPrev()">戻る</button>
          </div>
        </div>
    
      </div>
    </div>
    
    <script src="./script.js"></script>
    </body>
    </html>
    /* ステッパー サンプル — style.css
       色を変えたいときは :root の変数を書き換えるだけでOK */
    :root {
      --stp-color:  #2563EB;  /* 現在・完了ステップの色 */
      --stp-muted:  #E5E7EB;  /* 未着手のドット・ライン色 */
      --stp-text:   #9AA5B4;  /* 未着手のラベル色 */
    }
    
    *, *::before, *::after { box-sizing: border-box; }
    
    body {
      font-family: sans-serif;
      padding: 24px;
      max-width: 560px;
      margin: 0 auto;
      color: #1A2332;
    }
    
    /* ===== ステップインジケーター ===== */
    .stp-indicator {
      display: flex;
      list-style: none;
      margin: 0 0 32px;
      padding: 0;
    }
    
    .stp-item {
      flex: 1;
      display: flex;
      flex-direction: column;
      align-items: center;
      position: relative;
    }
    
    /* ステップ間の接続ライン(::before で前のステップとつなぐ) */
    .stp-item + .stp-item::before {
      content: '';
      position: absolute;
      top: 13px;      /* ドットの中心の高さ */
      right: 50%;     /* 自分の中心から */
      width: 100%;    /* 前のステップの中心まで */
      height: 2px;
      background: var(--stp-muted);
      z-index: 0;
    }
    
    /* 完了済みステップの後のラインをブルーにする */
    .stp-item.is-done + .stp-item::before {
      background: var(--stp-color);
    }
    
    /* ステップの丸(ドット) */
    .stp-dot {
      width: 28px;
      height: 28px;
      border-radius: 50%;
      background: #fff;
      border: 2px solid var(--stp-muted);
      color: var(--stp-text);
      font-size: 12px;
      font-weight: 700;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
      z-index: 1;
      transition: background 0.2s, border-color 0.2s, color 0.2s;
    }
    
    /* 現在のステップ */
    .stp-item.is-active .stp-dot {
      background: var(--stp-color);
      border-color: var(--stp-color);
      color: #fff;
    }
    
    /* 完了済みのステップ */
    .stp-item.is-done .stp-dot {
      background: var(--stp-color);
      border-color: var(--stp-color);
      color: #fff;
    }
    
    /* ステップラベル */
    .stp-label {
      font-size: 11px;
      color: var(--stp-text);
      margin-top: 5px;
      text-align: center;
    }
    
    .stp-item.is-active .stp-label {
      color: var(--stp-color);
      font-weight: 700;
    }
    
    .stp-item.is-done .stp-label { color: #5A6A7A; }
    
    /* ===== ステップコンテンツ ===== */
    .stp-step-title {
      font-size: 17px;
      font-weight: 700;
      margin: 0 0 8px;
    }
    
    .stp-step-desc {
      font-size: 14px;
      color: #5A6A7A;
      margin: 0 0 20px;
      line-height: 1.6;
    }
    
    /* ===== フィールドセット ===== */
    .stp-fieldset {
      border: none;
      padding: 0;
      margin: 0 0 16px;
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    
    .stp-choice-label {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 11px 14px;
      border: 1.5px solid #E5E7EB;
      border-radius: 8px;
      font-size: 14px;
      cursor: pointer;
      transition: border-color 0.15s, background 0.15s;
    }
    
    .stp-choice-label:hover {
      border-color: var(--stp-color);
      background: #F0F5FF;
    }
    
    .stp-choice-label input { accent-color: var(--stp-color); }
    
    /* ===== テキストエリア ===== */
    .stp-textarea {
      width: 100%;
      padding: 12px;
      border: 1.5px solid #E5E7EB;
      border-radius: 8px;
      font-size: 14px;
      font-family: sans-serif;
      resize: vertical;
      margin-bottom: 16px;
    }
    
    .stp-textarea:focus {
      outline: none;
      border-color: var(--stp-color);
      box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
    }
    
    /* ===== エラーメッセージ ===== */
    .stp-error {
      color: #DC2626;
      font-size: 13px;
      margin: -8px 0 12px;
    }
    
    /* ===== ボタン ===== */
    .stp-actions { display: flex; gap: 10px; margin-top: 8px; }
    
    .stp-btn-primary {
      padding: 10px 24px;
      background: var(--stp-color);
      color: #fff;
      border: none;
      border-radius: 6px;
      font-size: 14px;
      font-family: sans-serif;
      cursor: pointer;
      transition: background 0.15s;
    }
    
    .stp-btn-primary:hover { background: #1D4ED8; }
    
    .stp-btn-secondary {
      padding: 10px 24px;
      background: #fff;
      color: #5A6A7A;
      border: 1.5px solid #D0D7E0;
      border-radius: 6px;
      font-size: 14px;
      font-family: sans-serif;
      cursor: pointer;
      transition: background 0.15s;
    }
    
    .stp-btn-secondary:hover { background: #F4F6F9; }
    
    /* ===== 完了画面 ===== */
    .stp-complete-icon {
      width: 56px;
      height: 56px;
      background: #22C55E;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      margin: 0 auto 16px;
      color: #fff;
      font-size: 28px;
    }
    
    .stp-complete-message {
      text-align: center;
      font-size: 18px;
      font-weight: 700;
      margin: 0 0 24px;
    }
    
    /* ===== 回答確認 ===== */
    .stp-confirm {
      background: #F4F6F9;
      border-radius: 8px;
      padding: 20px;
      margin-bottom: 20px;
    }
    
    .stp-confirm-item { margin-bottom: 16px; }
    .stp-confirm-item:last-child { margin-bottom: 0; }
    
    .stp-confirm-label {
      font-size: 11px;
      font-weight: 700;
      color: #5A6A7A;
      margin: 0 0 3px;
    }
    
    .stp-confirm-value {
      font-size: 14px;
      color: #1A2332;
      margin: 0;
    }
    
    /* スクリーンリーダー専用(アクセシビリティ) */
    .stp-visually-hidden {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    }
    
    /* ===== スマホ対応 ===== */
    @media (max-width: 480px) {
      /* スマホではラベルを隠してドットのみ表示 */
      .stp-label { display: none; }
      .stp-dot { width: 24px; height: 24px; font-size: 11px; }
      .stp-item + .stp-item::before { top: 11px; }
    }
    // ステップ定義(ラベルはインジケーターに表示される)
    var STEPS = [
      { label: '説明' },
      { label: '職業' },
      { label: '技術' },
      { label: '要望' },
      { label: '完了' },
    ];
    
    var currentStep = 1; // 現在表示中のステップ番号
    var answers     = {}; // 各ステップの回答を格納
    
    // =========================================
    // インジケーターを描画する
    // =========================================
    function renderIndicator() {
      var list = document.getElementById('stp-indicator');
      list.innerHTML = '';
      STEPS.forEach(function(step, i) {
        var n  = i + 1;
        var li = document.createElement('li');
        li.className = 'stp-item';
        if (n < currentStep)  li.classList.add('is-done');   // 完了済み
        if (n === currentStep) li.classList.add('is-active'); // 現在
    
        // ドット(完了済みはチェックマーク、それ以外は番号)
        var dot = document.createElement('span');
        dot.className   = 'stp-dot';
        dot.textContent = (n < currentStep) ? '✓' : n;
    
        var label = document.createElement('span');
        label.className   = 'stp-label';
        label.textContent = step.label;
    
        li.appendChild(dot);
        li.appendChild(label);
        list.appendChild(li);
      });
    }
    
    // =========================================
    // 指定ステップだけを表示する
    // =========================================
    function showStep(n) {
      for (var i = 1; i <= STEPS.length; i++) {
        var el = document.getElementById('stp-step-' + i);
        if (el) el.hidden = (i !== n);
      }
    }
    
    // =========================================
    // バリデーション(true = 通過)
    // =========================================
    function validate(step) {
      if (step === 2) {
        var selected = document.querySelector('input[name="job"]:checked');
        var err      = document.getElementById('stp-error-2');
        if (!selected) { err.hidden = false; return false; }
        err.hidden = true;
      }
      if (step === 3) {
        var checked = document.querySelectorAll('input[name="tech"]:checked');
        var err     = document.getElementById('stp-error-3');
        if (checked.length === 0) { err.hidden = false; return false; }
        err.hidden = true;
      }
      return true; // Step 1・4・5 はバリデーション不要
    }
    
    // =========================================
    // 回答を answers オブジェクトに保存する
    // =========================================
    function saveAnswer(step) {
      if (step === 2) {
        var el    = document.querySelector('input[name="job"]:checked');
        answers.job = el ? el.value : '';
      }
      if (step === 3) {
        var els     = document.querySelectorAll('input[name="tech"]:checked');
        answers.tech = Array.from(els).map(function(el) { return el.value; });
      }
      if (step === 4) {
        answers.comment = document.getElementById('stp-comment').value.trim();
      }
    }
    
    // =========================================
    // 次へ
    // =========================================
    function goNext() {
      if (!validate(currentStep)) return;
      saveAnswer(currentStep);
      currentStep++;
      // 最終ステップ(Step 5)に入ったら確認画面を描画する
      if (currentStep === STEPS.length) renderConfirm();
      showStep(currentStep);
      renderIndicator();
    }
    
    // =========================================
    // 戻る
    // =========================================
    function goPrev() {
      if (currentStep <= 1) return;
      currentStep--;
      showStep(currentStep);
      renderIndicator();
    }
    
    // =========================================
    // XSS対策: HTMLエスケープ
    // ユーザー入力を innerHTML に渡す前に必ず通す
    // =========================================
    function escapeHtml(str) {
      return String(str)
        .replace(/&/g, '&amp;')
        .replace(/</g,  '&lt;')
        .replace(/>/g,  '&gt;')
        .replace(/"/g,  '&quot;');
    }
    
    // =========================================
    // 確認画面を描画する(Step 5 で呼び出す)
    // =========================================
    function renderConfirm() {
      var container   = document.getElementById('stp-confirm');
      var techText    = (answers.tech && answers.tech.length > 0)
        ? answers.tech.map(escapeHtml).join('、')
        : '(未回答)';
      var commentText = answers.comment
        ? escapeHtml(answers.comment)
        : '(なし)';
    
      container.innerHTML =
        '<div class="stp-confirm-item">' +
          '<p class="stp-confirm-label">Q1. 職業</p>' +
          '<p class="stp-confirm-value">' + escapeHtml(answers.job || '(未回答)') + '</p>' +
        '</div>' +
        '<div class="stp-confirm-item">' +
          '<p class="stp-confirm-label">Q2. フロントエンド技術</p>' +
          '<p class="stp-confirm-value">' + techText + '</p>' +
        '</div>' +
        '<div class="stp-confirm-item">' +
          '<p class="stp-confirm-label">Q3. 要望・ご意見</p>' +
          '<p class="stp-confirm-value">' + commentText + '</p>' +
        '</div>';
    }
    
    // =========================================
    // デモをリセットする
    // =========================================
    function resetDemo() {
      currentStep = 1;
      answers     = {};
      document.querySelectorAll('input[name="job"]').forEach(function(el)  { el.checked = false; });
      document.querySelectorAll('input[name="tech"]').forEach(function(el) { el.checked = false; });
      var comment = document.getElementById('stp-comment');
      if (comment) comment.value = '';
      document.querySelectorAll('.stp-error').forEach(function(el) { el.hidden = true; });
      showStep(1);
      renderIndicator();
    }
    
    // =========================================
    // 初期化
    // =========================================
    renderIndicator();
    showStep(1);

    AI用プロンプト

    以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。

    ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ステップ数や質問内容の変更など、要件を追記して使うのがおすすめです。

    ※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。

    💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。

    # ステッパー(アンケートウィザード)作成依頼
    
    ## 概要
    5ステップのアンケートフォームを通じて、ステッパー(ステップインジケーター)を実装してください。
    画面上部に「全何ステップ中、今何ステップ目か」を示すインジケーターを表示します。
    
    ## 要件
    - 全5ステップ構成(説明 → 択一設問 → 複数選択設問 → テキスト入力 → 完了・確認)
    - 画面上部にステップインジケーターを表示(ドット+接続ライン)
    - 現在ステップは青・強調、完了済みはチェックマーク付き、未着手はグレー
    - 「次へ」「戻る」ボタンでステップ間を移動
    - 必須回答がない場合は次へ進めず、エラーメッセージを表示
    - 最終ステップで全回答内容をまとめて確認表示する
    
    ## 技術仕様
    - HTML / CSS / バニラJavaScript で実装
    - 外部ライブラリ:なし
    - レスポンシブ対応:必要
    
    ## 動作詳細
    【Step 1】アンケートの説明文と「回答を始める」ボタンを表示。
    【Step 2】択一設問(ラジオボタン)。未選択で「次へ」するとエラーメッセージを表示。
    【Step 3】複数選択設問(チェックボックス)。1つ以上の選択が必須。
    【Step 4】自由記述(textarea)。任意入力のため未入力でも次へ進める。
    【Step 5】完了メッセージ+Step 2〜4の回答内容を一覧表示。
    ステップ切り替えは hidden 属性のON/OFFで行う。
    回答内容の表示には XSS 対策として必ず escapeHtml() を通すこと。
    
    ## 出力形式
    HTML・CSS・JavaScriptを分けて出力してください。
    各ファイルは単独でコピー&ペーストして使えるよう記述してください。