SnippetUI

マルチステップフォーム

登録画面などでよく使われる、複数ステップを切り替えるUI(プログレスインジケーター付き)

HTMLTailwind CSSJavaScript

ユーザー登録やアンケートなど、入力項目が多い画面で利用されるマルチステップフォーム(ウィザード形式)のUIコンポーネントです。 ステップごとの進捗状況を視覚的に伝えるプログレスインジケーターと、滑らかなスライドアニメーションを備えています。

プレビュー (Preview)

1
Account
2
Personal
3
Review

アカウント情報

ログインに使用する情報を入力してください。

個人情報

お客様の基本情報をお知らせください。

入力内容の確認

以下の内容で登録します。よろしいですか?

メールアドレスyou@example.com
氏名山田 太郎
電話番号090-1234-5678

ユースケースと特徴 (Use Cases & Features)

  • ユーザー登録・オンボーディング: 情報を分割して入力させることで、ユーザーの心理的ハードルを下げ、途中での離脱(カゴ落ち・フォーム落ち)を防ぎます。
  • 複雑な入力の整理: 配送先情報の入力やアンケートなど、項目が多いタスクを論理的なグループに分けることで、ユーザーの認知負荷を軽減します。
  • 進捗の可視化: 画面上部のプログレスインジケーターにより、「今どこにいるのか」「あとどれくらいで完了するのか」が一目で分かり、ユーザーに安心感を与えます。

デザインとUXのポイント (Design & UX)

  • スムーズなスライドアニメーション: ステップを切り替える際、前後のコンテンツが左右にフェードイン・フェードアウトしながらスライドすることで、空間的な連続性を演出しています。
  • 明確な状態フィードバック: 完了済みのステップにはチェックマークを表示し、現在のステップは ring ユーティリティでハイライト強調することで、現在のステータスを直感的に伝えます。
  • アクションを促すボタン設計: 最終ステップに到達すると、「次へ」ボタンの色が完了を意味する緑色系(emerald)に変化し、同時にラベルも「送信する」に切り替わることで、最終アクションを明確に促します。

カスタマイズ方法 (Customization Guide)

  • ブランドカラーへの適合: コード内の indigo-600 などの色指定をプロジェクトのテーマカラー(例: blue-600rose-500)に一括置換するだけで、簡単に色調をカスタマイズできます。
  • ステップ数の変更: HTML内の <div class="msf-step-indicator">...</div> および <div class="msf-step-content">...</div> のセットを増減させ、JavaScript内の const steps = [1, 2, 3]; を合わせることで、ステップ数を柔軟に変更可能です。
  • バリデーションの統合: 「次へ」ボタンのクリックイベント (btnNext.addEventListener) 内に、現在のステップごとの入力チェック(必須項目・フォーマットチェック等)を実装し、エラーがなければ currentStep++ するという制御を加えることで、実運用に対応できます。

実装コード (Implementation)

<div class="multi-step-form-wrapper bg-gray-50 dark:bg-zinc-950 flex items-center justify-center p-6 min-h-[600px] font-sans">
<div class="bg-white dark:bg-zinc-900 rounded-2xl shadow-xl w-full max-w-lg p-8 relative overflow-hidden border border-gray-100 dark:border-zinc-800">
  <!-- Progress Bar -->
  <div class="flex justify-between items-center mb-10 relative">
    <div class="absolute left-0 top-1/2 transform -translate-y-1/2 w-full h-1.5 bg-gray-200 dark:bg-zinc-800 z-0 rounded-full"></div>
    <div id="msf-progress-bar" class="absolute left-0 top-1/2 transform -translate-y-1/2 h-1.5 bg-indigo-600 z-0 transition-all duration-500 ease-out rounded-full" style="width: 0%;"></div>
    
    <!-- Step 1 -->
    <div class="relative z-10 flex flex-col items-center">
      <div class="msf-step-indicator w-10 h-10 rounded-full flex items-center justify-center font-bold transition-all duration-500 bg-indigo-600 text-white shadow-md border-4 border-white dark:border-zinc-900 ring-4 ring-indigo-100" data-step="1">1</div>
      <span class="msf-step-label text-xs font-bold text-indigo-600 mt-2 transition-colors duration-300 tracking-wide uppercase">Account</span>
    </div>
    <!-- Step 2 -->
    <div class="relative z-10 flex flex-col items-center">
      <div class="msf-step-indicator w-10 h-10 rounded-full flex items-center justify-center font-bold transition-all duration-500 bg-gray-200 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 dark:text-gray-500 border-4 border-white dark:border-zinc-900" data-step="2">2</div>
      <span class="msf-step-label text-xs font-medium text-gray-400 dark:text-gray-500 mt-2 transition-colors duration-300 tracking-wide uppercase">Personal</span>
    </div>
    <!-- Step 3 -->
    <div class="relative z-10 flex flex-col items-center">
      <div class="msf-step-indicator w-10 h-10 rounded-full flex items-center justify-center font-bold transition-all duration-500 bg-gray-200 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 dark:text-gray-500 border-4 border-white dark:border-zinc-900" data-step="3">3</div>
      <span class="msf-step-label text-xs font-medium text-gray-400 dark:text-gray-500 mt-2 transition-colors duration-300 tracking-wide uppercase">Review</span>
    </div>
  </div>

  <!-- Form Steps -->
  <div class="relative h-[280px] overflow-visible">
    <!-- Step 1 Content -->
    <div class="msf-step-content absolute w-full transition-all duration-500 transform translate-x-0 opacity-100" data-step="1">
      <h2 class="text-2xl font-extrabold text-gray-900 dark:text-white mb-2">アカウント情報</h2>
      <p class="text-sm text-gray-500 dark:text-gray-400 dark:text-gray-500 mb-6">ログインに使用する情報を入力してください。</p>
      <div class="space-y-4">
        <div>
          <label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">メールアドレス</label>
          <input type="email" required class="w-full text-gray-900 dark:text-white placeholder-gray-400 px-4 py-2.5 border border-gray-300 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all shadow-sm bg-gray-50 dark:bg-zinc-950 focus:bg-white dark:bg-zinc-900" placeholder="you@example.com" />
        </div>
        <div>
          <label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">パスワード</label>
          <input type="password" required class="w-full text-gray-900 dark:text-white placeholder-gray-400 px-4 py-2.5 border border-gray-300 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all shadow-sm bg-gray-50 dark:bg-zinc-950 focus:bg-white dark:bg-zinc-900" placeholder="••••••••" />
        </div>
      </div>
    </div>

    <!-- Step 2 Content -->
    <div class="msf-step-content absolute w-full transition-all duration-500 transform translate-x-12 opacity-0 pointer-events-none" data-step="2">
      <h2 class="text-2xl font-extrabold text-gray-900 dark:text-white mb-2">個人情報</h2>
      <p class="text-sm text-gray-500 dark:text-gray-400 dark:text-gray-500 mb-6">お客様の基本情報をお知らせください。</p>
      <div class="space-y-4">
        <div class="grid grid-cols-2 gap-4">
          <div>
            <label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">姓</label>
            <input type="text" required class="w-full text-gray-900 dark:text-white placeholder-gray-400 px-4 py-2.5 border border-gray-300 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all shadow-sm bg-gray-50 dark:bg-zinc-950 focus:bg-white dark:bg-zinc-900" placeholder="山田" />
          </div>
          <div>
            <label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">名</label>
            <input type="text" required class="w-full text-gray-900 dark:text-white placeholder-gray-400 px-4 py-2.5 border border-gray-300 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all shadow-sm bg-gray-50 dark:bg-zinc-950 focus:bg-white dark:bg-zinc-900" placeholder="太郎" />
          </div>
        </div>
        <div>
          <label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1.5">電話番号</label>
          <input type="tel" required class="w-full text-gray-900 dark:text-white placeholder-gray-400 px-4 py-2.5 border border-gray-300 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all shadow-sm bg-gray-50 dark:bg-zinc-950 focus:bg-white dark:bg-zinc-900" placeholder="090-1234-5678" />
        </div>
      </div>
    </div>

    <!-- Step 3 Content -->
    <div class="msf-step-content absolute w-full transition-all duration-500 transform translate-x-12 opacity-0 pointer-events-none" data-step="3">
      <h2 class="text-2xl font-extrabold text-gray-900 dark:text-white mb-2">入力内容の確認</h2>
      <p class="text-sm text-gray-500 dark:text-gray-400 dark:text-gray-500 mb-6">以下の内容で登録します。よろしいですか?</p>
      <div class="bg-gray-50 dark:bg-zinc-950/80 backdrop-blur-sm rounded-xl p-5 border border-gray-100 dark:border-zinc-800 shadow-inner space-y-4 text-sm">
        <div class="flex justify-between items-center border-b border-gray-200 dark:border-zinc-800 pb-3">
          <span class="text-gray-500 dark:text-gray-400 dark:text-gray-500 font-medium">メールアドレス</span>
          <span class="font-bold text-gray-800 dark:text-gray-200">you@example.com</span>
        </div>
        <div class="flex justify-between items-center border-b border-gray-200 dark:border-zinc-800 pb-3">
          <span class="text-gray-500 dark:text-gray-400 dark:text-gray-500 font-medium">氏名</span>
          <span class="font-bold text-gray-800 dark:text-gray-200">山田 太郎</span>
        </div>
        <div class="flex justify-between items-center">
          <span class="text-gray-500 dark:text-gray-400 dark:text-gray-500 font-medium">電話番号</span>
          <span class="font-bold text-gray-800 dark:text-gray-200">090-1234-5678</span>
        </div>
      </div>
    </div>
  </div>

  <!-- Buttons -->
  <div class="flex justify-between mt-8 pt-6 border-t border-gray-100 dark:border-zinc-800 relative z-10">
    <button id="msf-btn-prev" class="px-6 py-2.5 border border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-gray-300 font-bold rounded-xl hover:bg-gray-50 dark:bg-zinc-950 hover:text-indigo-600 focus:outline-none focus:ring-2 focus:ring-gray-200 transition-all duration-300 opacity-0 pointer-events-none disabled:opacity-50 flex items-center justify-center gap-2">
      <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path></svg>
      戻る
    </button>
    <button id="msf-btn-next" class="px-6 py-2.5 bg-indigo-600 text-white font-bold rounded-xl hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-all duration-300 ml-auto shadow-[0_4px_14px_0_rgba(79,70,229,0.39)] hover:shadow-[0_6px_20px_rgba(79,70,229,0.23)] hover:-translate-y-0.5 flex items-center justify-center gap-2 active:translate-y-0">
      次へ
      <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
    </button>
  </div>
</div>
</div>

<script>
(function() {
  const steps = [1, 2, 3];
  let currentStep = 1;
  
  const progressBar = document.getElementById('msf-progress-bar');
  const indicators = document.querySelectorAll('.msf-step-indicator');
  const labels = document.querySelectorAll('.msf-step-label');
  const contents = document.querySelectorAll('.msf-step-content');
  const btnPrev = document.getElementById('msf-btn-prev');
  const btnNext = document.getElementById('msf-btn-next');
  
  if (!progressBar) return;

  function validateStep() {
    if (currentStep === steps.length) return true;
    const currentContent = document.querySelector('.msf-step-content[data-step="' + currentStep + '"]');
    const inputs = currentContent.querySelectorAll('input[required]');
    let isValid = true;
    inputs.forEach(input => {
      if (!input.checkValidity()) {
        input.reportValidity();
        isValid = false;
      }
    });
    return isValid;
  }

  function updateUI() {
    const progressPercentage = ((currentStep - 1) / (steps.length - 1)) * 100;
    progressBar.style.width = progressPercentage + '%';
    
    indicators.forEach((ind, index) => {
      const stepNum = index + 1;
      const label = labels[index];
      
      if (stepNum < currentStep) {
        ind.className = "msf-step-indicator w-10 h-10 rounded-full flex items-center justify-center font-bold transition-all duration-500 bg-indigo-600 text-white shadow-md border-4 border-white dark:border-zinc-900";
        ind.innerHTML = '<svg class="w-5 h-5 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7"></path></svg>';
        label.className = "msf-step-label text-xs font-bold text-indigo-600 mt-2 transition-colors duration-300 tracking-wide uppercase";
      } else if (stepNum === currentStep) {
        ind.className = "msf-step-indicator w-10 h-10 rounded-full flex items-center justify-center font-bold transition-all duration-500 bg-indigo-600 text-white shadow-md border-4 border-white dark:border-zinc-900 ring-4 ring-indigo-100 scale-110";
        ind.innerHTML = stepNum;
        label.className = "msf-step-label text-xs font-bold text-indigo-600 mt-2 transition-colors duration-300 tracking-wide uppercase";
      } else {
        ind.className = "msf-step-indicator w-10 h-10 rounded-full flex items-center justify-center font-bold transition-all duration-500 bg-gray-200 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 dark:text-gray-500 border-4 border-white dark:border-zinc-900";
        ind.innerHTML = stepNum;
        label.className = "msf-step-label text-xs font-medium text-gray-400 dark:text-gray-500 mt-2 transition-colors duration-300 tracking-wide uppercase";
      }
    });
    
    contents.forEach((content, index) => {
      const stepNum = index + 1;
      if (stepNum === currentStep) {
        content.className = "msf-step-content absolute w-full transition-all duration-500 transform translate-x-0 opacity-100 delay-100";
      } else if (stepNum < currentStep) {
        content.className = "msf-step-content absolute w-full transition-all duration-500 transform -translate-x-12 opacity-0 pointer-events-none";
      } else {
        content.className = "msf-step-content absolute w-full transition-all duration-500 transform translate-x-12 opacity-0 pointer-events-none";
      }
    });
    
    if (currentStep === 1) {
      btnPrev.className = "px-6 py-2.5 border border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-gray-300 font-bold rounded-xl transition-all duration-300 opacity-0 pointer-events-none absolute left-0";
    } else {
      btnPrev.className = "px-6 py-2.5 border border-gray-300 dark:border-zinc-700 text-gray-700 dark:text-gray-300 font-bold rounded-xl hover:bg-gray-50 dark:bg-zinc-950 hover:text-indigo-600 focus:outline-none focus:ring-2 focus:ring-gray-200 transition-all duration-300 opacity-100 flex items-center justify-center gap-2";
    }
    
    if (currentStep === steps.length) {
      btnNext.innerHTML = '送信する <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>';
      btnNext.className = "px-6 py-2.5 bg-emerald-500 text-white font-bold rounded-xl hover:bg-emerald-600 focus:outline-none focus:ring-2 focus:ring-emerald-400 focus:ring-offset-2 transition-all duration-300 ml-auto shadow-[0_4px_14px_0_rgba(16,185,129,0.39)] hover:shadow-[0_6px_20px_rgba(16,185,129,0.23)] hover:-translate-y-0.5 flex items-center justify-center gap-2 active:translate-y-0";
    } else {
      btnNext.innerHTML = '次へ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>';
      btnNext.className = "px-6 py-2.5 bg-indigo-600 text-white font-bold rounded-xl hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-all duration-300 ml-auto shadow-[0_4px_14px_0_rgba(79,70,229,0.39)] hover:shadow-[0_6px_20px_rgba(79,70,229,0.23)] hover:-translate-y-0.5 flex items-center justify-center gap-2 active:translate-y-0";
    }
  }

  btnPrev.addEventListener('click', () => {
    if (currentStep > 1) {
      currentStep--;
      updateUI();
    }
  });
  
  btnNext.addEventListener('click', () => {
    if (!validateStep()) return;
    
    if (currentStep < steps.length) {
      currentStep++;
      updateUI();
    } else {
      btnNext.innerHTML = '<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white inline-block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>送信中...';
      setTimeout(() => {
        alert('フォームが送信されました!');
        currentStep = 1;
        updateUI();
      }, 1500);
    }
  });
  
  updateUI();
})();
</script>

ブラウザ対応状況 (Browser Support)

モダンブラウザ(Chrome, Safari, Firefox, Edge)の最新バージョンで正常に動作します。アニメーションにはCSS TransitionsとTailwindのユーティリティクラスを使用しており、JavaScriptは状態管理(クラスの付け替えと進捗バーの幅計算のみ)を行っているため、軽量かつ高速に動作します。