ステップ入力ウィザード(複数ステップフォーム)

応用例 中級

この画面パターンについて

会員登録・申込フローなど、入力項目が多い場面で使う「複数ステップに分割したフォーム」のパターンです。 ステッパーで現在位置を示しながら、入力 → 詳細 → 確認 → 完了の4ステップを進みます。 ステップごとの検証・戻ったときの入力保持・確認画面からの修正導線という、ウィザードUIで必ず問われる3点をまとめて実装します。

こんな場面で使えます

  • 会員登録・申込フォーム — 長い入力を分割して離脱を防ぐ
  • 見積もり・申請フォーム — 入力内容を最後に確認してから送信させる
  • 初期設定ウィザード — 導入時の設定を順番に案内する

この画面で使っているUIコンポーネント

#パーツこの画面での役割
1ステッパー現在位置と完了状態を表示
2Step1フォーム(テキスト+セレクト)名前・メール・区分の入力
3Step2フォーム(ラジオチェック+テキストエリア)プラン・オプション・備考の入力
4ステップ単位バリデーション「次へ」時に現在ステップだけ検証
5戻る/次へナビゲーションステップ間の移動
6確認画面(定義リスト+修正リンク)全入力の確認と修正導線
7完了画面チェックアイコン+完了メッセージ
8リセット導線「最初からやり直す」で全初期化

実装のポイント・注意点

進行は currentStep という1変数だけで管理し、goToStep(n) がパネルの表示切替・ステッパーの状態更新・ボタンの出し分けをまとめて引き受けます。 各ステップは hidden 属性で隠すだけなので、DOM上に入力値が残り「戻る」での入力保持を特別な処理なしで実現できます。これがウィザードを1ページ完結で作る最大の利点です。

バリデーションは「次へ」を押した時点で現在のステップだけ検証し、エラーなら進ませません。 確認画面は開くたびにDOMから現在値を読み直して再生成し、修正リンクで戻った場合も通常の「次へ」検証フローを通すことで、検証をすり抜けて送信される穴を塞ぎます。

8個のUIコンポーネントをHTML・CSS・バニラJavaScriptのみで組み合わせており、 フレームワーク不要で画面ごとコピペして使えます。

動作サンプル

ステップ入力ウィザード(複数ステップフォーム)のデモ画面 動作サンプルを別ウィンドウで確認 ↗

試してみる:

  • 何も入力せずに「次へ」を押して、ステップ単位のバリデーションを確認
  • Step2から「戻る」で戻り、入力値が保持されていることを確認
  • 確認画面の「修正する」リンクで該当ステップへ直接戻れることを確認

そのほかの操作も自由に試してみてください。

サンプルソース

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

<!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="wizard-screen">
  <!-- ===== ステッパー ===== -->
  <ol class="stepper" id="stepper">
    <li class="step is-current" data-step="1"><span class="step-num">1</span>基本情報</li>
    <li class="step" data-step="2"><span class="step-num">2</span>詳細選択</li>
    <li class="step" data-step="3"><span class="step-num">3</span>確認</li>
    <li class="step" data-step="4"><span class="step-num">4</span>完了</li>
  </ol>

  <!-- ===== Step1:基本情報 ===== -->
  <section class="wizard-panel" data-panel="1">
    <div class="form-field">
      <label for="nameInput">名前 <span class="required-mark">*</span></label>
      <input type="text" id="nameInput">
      <p class="field-error" id="nameError" hidden></p>
    </div>
    <div class="form-field">
      <label for="emailInput">メールアドレス <span class="required-mark">*</span></label>
      <input type="email" id="emailInput">
      <p class="field-error" id="emailError" hidden></p>
    </div>
    <div class="form-field">
      <label for="typeSelect">区分 <span class="required-mark">*</span></label>
      <select id="typeSelect">
        <option value="">選択してください</option>
        <option value="区分1">区分1</option>
        <option value="区分2">区分2</option>
        <option value="区分3">区分3</option>
      </select>
      <p class="field-error" id="typeError" hidden></p>
    </div>
  </section>

  <!-- ===== Step2:詳細選択 ===== -->
  <section class="wizard-panel" data-panel="2" hidden>
    <fieldset class="form-field">
      <legend>プラン <span class="required-mark">*</span></legend>
      <label><input type="radio" name="plan" value="プランA" checked> プランA</label>
      <label><input type="radio" name="plan" value="プランB"> プランB</label>
      <label><input type="radio" name="plan" value="プランC"> プランC</label>
    </fieldset>
    <fieldset class="form-field">
      <legend>オプション(任意)</legend>
      <label><input type="checkbox" name="option" value="オプション1"> オプション1</label>
      <label><input type="checkbox" name="option" value="オプション2"> オプション2</label>
      <label><input type="checkbox" name="option" value="オプション3"> オプション3</label>
    </fieldset>
    <div class="form-field">
      <label for="noteInput">備考(任意・200文字以内)</label>
      <textarea id="noteInput" rows="3"></textarea>
      <p class="field-error" id="noteError" hidden></p>
    </div>
  </section>

  <!-- ===== Step3:確認 ===== -->
  <section class="wizard-panel" data-panel="3" hidden>
    <div class="confirm-section">
      <h2 class="confirm-heading">基本情報 <button type="button" class="link-btn" data-goto="1">修正する</button></h2>
      <dl class="confirm-list" id="confirmBasic"><!-- JSで生成 --></dl>
    </div>
    <div class="confirm-section">
      <h2 class="confirm-heading">詳細選択 <button type="button" class="link-btn" data-goto="2">修正する</button></h2>
      <dl class="confirm-list" id="confirmDetail"><!-- JSで生成 --></dl>
    </div>
  </section>

  <!-- ===== Step4:完了 ===== -->
  <section class="wizard-panel wizard-done" data-panel="4" hidden>
    <div class="done-icon" aria-hidden="true"></div>
    <p class="done-message">送信が完了しました</p>
    <button type="button" class="btn-secondary" id="restartBtn">最初からやり直す</button>
  </section>

  <!-- ===== ナビゲーション ===== -->
  <div class="wizard-nav" id="wizardNav">
    <button type="button" class="btn-secondary" id="backBtn" hidden>← 戻る</button>
    <button type="button" class="btn-primary" id="nextBtn">次へ →</button>
  </div>
</div>

<script src="./script.js"></script>
</body>
</html>
/* ===== ステップ入力ウィザード — style.css ===== */
*, *::before, *::after { box-sizing: border-box; }

:root {
  --color-primary: #2B7FE8;
  --color-success: #22A06B;
  --color-danger:  #D64545;
  --color-text:    #1E293B;
  --color-muted:   #64748B;
  --color-border:  #D0D7E0;
  --color-bg:      #F4F6F9;
  --color-card:    #FFFFFF;
}

body {
  margin: 0;
  font-family: "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, sans-serif;
  background: var(--color-bg);
  color: var(--color-text);
}

/* ===== 画面レイアウト ===== */
.wizard-screen {
  max-width: 640px;
  margin: 0 auto;
  padding: 24px 20px 48px;
}

/* ===== ステッパー ===== */
.stepper {
  display: flex;
  list-style: none;
  margin: 0 0 24px;
  padding: 0;
}

.step {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--color-muted);
}

/* ステップ間をつなぐ横線 */
.step:not(:last-child)::after {
  content: "";
  position: absolute;
  top: 14px;
  left: calc(50% + 22px);
  width: calc(100% - 44px);
  height: 2px;
  background: var(--color-border);
}

/* 完了済みステップから伸びる線は緑にする */
.step.is-done:not(:last-child)::after { background: var(--color-success); }

.step-num {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  font-size: 13px;
  font-weight: 700;
  color: var(--color-muted);
  background: var(--color-card);
  border: 2px solid var(--color-border);
}

/* 現在のステップ(青強調) */
.step.is-current {
  color: var(--color-primary);
  font-weight: 700;
}

.step.is-current .step-num {
  color: #fff;
  background: var(--color-primary);
  border-color: var(--color-primary);
}

/* 完了済みステップ(緑)— 数字を消して ✓ に差し替える */
.step.is-done { color: var(--color-success); }

.step.is-done .step-num {
  font-size: 0;
  background: var(--color-success);
  border-color: var(--color-success);
}

.step.is-done .step-num::after {
  content: "✓";
  font-size: 14px;
  color: #fff;
}

/* ===== パネル(各ステップの中身) ===== */
.wizard-panel {
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: 24px;
}

/* ===== フォーム部品 ===== */
.form-field { margin-bottom: 18px; }
.form-field:last-child { margin-bottom: 0; }

.form-field > label,
.form-field > legend {
  display: block;
  margin: 0 0 6px;
  padding: 0;
  font-size: 14px;
  font-weight: 700;
}

.required-mark { color: var(--color-danger); }

.form-field input[type="text"],
.form-field input[type="email"],
.form-field textarea,
.form-field select {
  width: 100%;
  padding: 8px 10px;
  font-size: 14px;
  font-family: inherit;
  color: var(--color-text);
  background: var(--color-card);
  border: 1px solid var(--color-border);
  border-radius: 6px;
}

.form-field textarea { resize: vertical; }

.form-field input:focus-visible,
.form-field textarea:focus-visible,
.form-field select:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: -1px;
}

/* バリデーションエラー時は入力枠を赤くする */
.form-field .is-invalid { border-color: var(--color-danger); }

.field-error {
  margin: 4px 0 0;
  font-size: 12px;
  color: var(--color-danger);
}

/* ラジオ・チェックボックスの選択肢(fieldset 内の label) */
fieldset.form-field {
  border: none;
  margin: 0 0 18px;
  padding: 0;
  min-width: 0;
}

fieldset.form-field label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 4px 20px 4px 0;
  font-size: 14px;
  font-weight: 400;
  cursor: pointer;
}

fieldset.form-field input[type="radio"],
fieldset.form-field input[type="checkbox"] {
  accent-color: var(--color-primary);
}

/* ===== 確認画面 ===== */
.confirm-section { margin-bottom: 24px; }
.confirm-section:last-child { margin-bottom: 0; }

.confirm-heading {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 0 10px;
  padding-bottom: 8px;
  font-size: 15px;
  border-bottom: 1px solid #E6EBF1;
}

.link-btn {
  padding: 0;
  font-size: 12px;
  font-weight: 400;
  font-family: inherit;
  color: var(--color-primary);
  background: none;
  border: none;
  text-decoration: underline;
  cursor: pointer;
}

.link-btn:hover { color: #1D6AD0; }

.confirm-list {
  display: grid;
  grid-template-columns: 140px 1fr;
  gap: 8px 16px;
  margin: 0;
  font-size: 14px;
}

.confirm-list dt { color: var(--color-muted); }

/* 備考の改行をそのまま表示する */
.confirm-list dd {
  margin: 0;
  white-space: pre-wrap;
}

/* ===== 完了画面 ===== */
.wizard-done {
  text-align: center;
  padding: 48px 24px;
}

/* CSSだけで描くチェックアイコン(円+45度回転させたL字の線) */
.done-icon {
  position: relative;
  width: 64px;
  height: 64px;
  margin: 0 auto 16px;
  border: 3px solid var(--color-success);
  border-radius: 50%;
}

.done-icon::after {
  content: "";
  position: absolute;
  left: 21px;
  top: 13px;
  width: 15px;
  height: 28px;
  border-right: 3px solid var(--color-success);
  border-bottom: 3px solid var(--color-success);
  transform: rotate(45deg);
}

.done-message {
  margin: 0 0 24px;
  font-size: 17px;
  font-weight: 700;
}

/* ===== ナビゲーション ===== */
.wizard-nav {
  display: flex;
  justify-content: space-between;
  margin-top: 20px;
}

/* 「戻る」がないステップでも「次へ」を右端に置く */
.wizard-nav .btn-primary { margin-left: auto; }

.btn-primary,
.btn-secondary {
  padding: 10px 22px;
  font-size: 14px;
  font-family: inherit;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.btn-primary {
  color: #fff;
  background: var(--color-primary);
  border: 1.5px solid var(--color-primary);
}

.btn-primary:hover { background: #1D6AD0; }

.btn-secondary {
  color: var(--color-muted);
  background: var(--color-card);
  border: 1.5px solid var(--color-border);
}

.btn-secondary:hover {
  background: var(--color-bg);
  border-color: #9AA5B4;
}

/* hidden 属性を確実に効かせる(display 指定との競合対策) */
.wizard-screen [hidden] { display: none !important; }

/* ===== レスポンシブ ===== */
@media (max-width: 768px) {
  .wizard-panel { padding: 20px 16px; }
  .confirm-list { grid-template-columns: 110px 1fr; }
}

/* スマホ幅ではステッパーのラベルを番号のみに省略する */
@media (max-width: 480px) {
  .step {
    font-size: 0;
    gap: 0;
  }
}
/* =====================================================
   ステップ入力ウィザードのスクリプト

   仕組み:進行状態は currentStep の1変数だけで管理し、
   goToStep(n) がパネルの表示・ステッパーの状態・
   ナビボタンの出し分けをまとめて切り替える。

   各ステップは hidden 属性で隠すだけなので、DOM上に
   入力値が残り「戻る」での入力保持に特別な処理は不要。
   確認画面は開くたびに最新の入力値から再生成する。
   ===================================================== */

// ===== 設定値 =====
var NOTE_MAX_LENGTH = 200;                          // 備考の最大文字数
var EMAIL_PATTERN   = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // メール書式(実用レベルの簡易チェック)
var DEFAULT_PLAN    = 'プランA';                    // プランの初期選択

// ナビボタンの文言テーブル(ステップごとの表示設定。Step4は完了画面でナビ非表示)
var NAV_TABLE = {
  1: { back: false, next: '次へ →' },
  2: { back: true,  next: '次へ →' },
  3: { back: true,  next: '送信する' }
};

// ===== DOM要素 =====
var panels     = document.querySelectorAll('.wizard-panel');
var steps      = document.querySelectorAll('.stepper .step');
var wizardNav  = document.getElementById('wizardNav');
var backBtn    = document.getElementById('backBtn');
var nextBtn    = document.getElementById('nextBtn');
var restartBtn = document.getElementById('restartBtn');

var nameInput  = document.getElementById('nameInput');
var emailInput = document.getElementById('emailInput');
var typeSelect = document.getElementById('typeSelect');
var noteInput  = document.getElementById('noteInput');

var nameError  = document.getElementById('nameError');
var emailError = document.getElementById('emailError');
var typeError  = document.getElementById('typeError');
var noteError  = document.getElementById('noteError');

var confirmBasic  = document.getElementById('confirmBasic');
var confirmDetail = document.getElementById('confirmDetail');

// ===== ステップ切り替え =====
var currentStep = 1; // 進行状態はこの1変数だけで管理する

// パネルの表示・ステッパーの状態・ナビボタンをまとめて切り替える
function goToStep(n) {
  currentStep = n;

  // 確認画面は開くたびに最新の入力値から作り直す
  // (一度作った内容を使い回すと「修正する」で直した値が反映されない)
  if (n === 3) { renderConfirm(); }

  panels.forEach(function (panel) {
    panel.hidden = Number(panel.dataset.panel) !== n;
  });

  steps.forEach(function (step) {
    var stepNo = Number(step.dataset.step);
    step.classList.toggle('is-done', stepNo < n);
    step.classList.toggle('is-current', stepNo === n);
  });

  var nav = NAV_TABLE[n];
  wizardNav.hidden = !nav;
  if (nav) {
    backBtn.hidden = !nav.back;
    nextBtn.textContent = nav.next;
  }
}

// ===== バリデーション =====
// Step1:名前(必須)・メール(必須+書式)・区分(必須)を検証する
function validateStep1() {
  var errors = [];
  if (nameInput.value.trim() === '') {
    errors.push({ input: nameInput, errorEl: nameError, message: '名前を入力してください' });
  }
  var email = emailInput.value.trim();
  if (email === '') {
    errors.push({ input: emailInput, errorEl: emailError, message: 'メールアドレスを入力してください' });
  } else if (!EMAIL_PATTERN.test(email)) {
    errors.push({ input: emailInput, errorEl: emailError, message: 'メールアドレスの形式が正しくありません' });
  }
  if (typeSelect.value === '') {
    errors.push({ input: typeSelect, errorEl: typeError, message: '区分を選択してください' });
  }
  return errors;
}

// Step2:備考の文字数のみ検証する(プランは初期値あり・オプションは任意)
function validateStep2() {
  var errors = [];
  if (noteInput.value.length > NOTE_MAX_LENGTH) {
    errors.push({ input: noteInput, errorEl: noteError, message: NOTE_MAX_LENGTH + '文字以内で入力してください' });
  }
  return errors;
}

// エラーをフィールド直下に表示し、最初のエラーへフォーカスを移す
function showErrors(errors) {
  clearErrors();
  errors.forEach(function (error) {
    error.input.classList.add('is-invalid');
    error.errorEl.textContent = error.message;
    error.errorEl.hidden = false;
  });
  if (errors.length > 0) { errors[0].input.focus(); }
}

// すべてのエラー表示を消す
function clearErrors() {
  document.querySelectorAll('.field-error').forEach(function (el) {
    el.hidden = true;
    el.textContent = '';
  });
  document.querySelectorAll('.is-invalid').forEach(function (el) {
    el.classList.remove('is-invalid');
  });
}

// ===== 確認画面の生成 =====
// 現在の入力値を読み直して「基本情報」「詳細選択」の定義リストを作る
function renderConfirm() {
  renderList(confirmBasic, [
    ['名前', nameInput.value],
    ['メールアドレス', emailInput.value],
    ['区分', typeSelect.value]
  ]);

  var plan = document.querySelector('input[name="plan"]:checked').value;

  // チェック済みオプションを集めて「、」区切りに。0件なら「なし」
  var options = [];
  document.querySelectorAll('input[name="option"]:checked').forEach(function (checkbox) {
    options.push(checkbox.value);
  });
  var optionText = options.length > 0 ? options.join('、') : 'なし';

  var noteText = noteInput.value.trim() !== '' ? noteInput.value : '(未入力)';

  renderList(confirmDetail, [
    ['プラン', plan],
    ['オプション', optionText],
    ['備考', noteText]
  ]);
}

// ラベルと値の組から dt/dd を生成する(createElement + textContent で XSS 対策)
function renderList(listEl, rows) {
  listEl.textContent = ''; // 前回の内容を消してから作り直す
  rows.forEach(function (row) {
    var dt = document.createElement('dt');
    dt.textContent = row[0];
    var dd = document.createElement('dd');
    dd.textContent = row[1];
    listEl.appendChild(dt);
    listEl.appendChild(dd);
  });
}

// ===== ナビゲーション =====
// 「次へ/送信する」クリック → 現在のステップだけ検証し、エラーがなければ進む
nextBtn.addEventListener('click', function () {
  var errors = [];
  if (currentStep === 1) { errors = validateStep1(); }
  if (currentStep === 2) { errors = validateStep2(); }

  if (errors.length > 0) {
    showErrors(errors);
    return; // エラーがある間はステップを進めない
  }

  clearErrors();
  goToStep(currentStep + 1);
});

// 「← 戻る」クリック → 前のステップへ(入力値はDOMに残っているので保持される)
backBtn.addEventListener('click', function () {
  goToStep(currentStep - 1);
});

// 確認画面の「修正する」クリック → 該当ステップへ戻る
// (戻った後はまた通常の「次へ」検証フローを通る=再検証漏れを防ぐ)
document.querySelectorAll('.link-btn[data-goto]').forEach(function (btn) {
  btn.addEventListener('click', function () {
    goToStep(Number(btn.dataset.goto));
  });
});

// ===== リセット =====
// 「最初からやり直す」クリック → 全フォームを初期化して Step1 へ戻る
restartBtn.addEventListener('click', function () {
  nameInput.value = '';
  emailInput.value = '';
  typeSelect.value = '';
  document.querySelector('input[name="plan"][value="' + DEFAULT_PLAN + '"]').checked = true;
  document.querySelectorAll('input[name="option"]').forEach(function (checkbox) {
    checkbox.checked = false;
  });
  noteInput.value = '';
  clearErrors();
  goToStep(1);
});

// ===== 初期化 =====
goToStep(1);

AI用プロンプト

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

ChatGPTやClaudeにこのプロンプトを渡すと、同様の画面をゼロから生成・カスタマイズできます。ステップの追加や入力項目の変更など、要件を追記して使うのがおすすめです。

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

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

# ステップ入力ウィザード 作成依頼

## 概要
入力→詳細→確認→完了の4ステップで進む複数ステップフォーム(ウィザード)を作成してください。
ページ遷移なしの1ページ内で、ステッパー表示とともにステップを切り替えます。

## 要件
- ステッパー(1 基本情報 → 2 詳細選択 → 3 確認 → 4 完了)を上部に表示し、
  完了済み(チェックマーク)・現在(強調色)・未到達(グレー)の3状態を表現する
- Step1:名前(必須)・メールアドレス(必須・形式チェック)・区分(セレクト・必須)
- Step2:プラン(ラジオ3択・初期選択あり)・オプション(チェックボックス3つ・任意)・
  備考(テキストエリア・任意・200文字以内)
- 「次へ」を押した時点でそのステップのみバリデーションし、エラーは
  フィールド直下に赤字表示して進ませない
- 「戻る」で前のステップに戻っても入力値は保持される
- Step3(確認):全入力をラベル+値の一覧で表示。「基本情報」「詳細選択」の
  セクションごとに「修正する」リンクを置き、該当ステップへ戻れる。
  オプション未選択は「なし」、備考未入力は「(未入力)」と表示する
- 「送信する」で完了画面(チェックアイコン+「送信が完了しました」)を表示する。
  実際のサーバー送信は行わない
- 完了画面の「最初からやり直す」で全入力をリセットしてStep1に戻る

## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし(チェックアイコンもCSSで描画する)
- レスポンシブ対応:必要(スマホ幅ではステッパーのラベルを番号のみに省略)

## 動作詳細
- 各ステップは同一ページ内のセクションを hidden 属性で切り替えて表示する
  (入力値の保持はDOMに任せ、値の退避・復元コードを書かない)
- 現在のステップ番号は変数1つで管理し、パネル表示・ステッパー状態・
  ナビボタンの切り替えを1つの関数にまとめる
- 確認画面の内容は表示するたびに最新の入力値から生成する
- DOM生成は createElement と textContent を使い、innerHTML に変数を結合しない

## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。