T-CREATOR

htmx で多段階ウィザードフォームを実装するコツ

htmx で多段階ウィザードフォームを実装するコツ

Web アプリケーション開発において、複雑なフォームを扱う際に避けて通れないのが「多段階ウィザードフォーム」です。従来の SPA(Single Page Application)では、状態管理の複雑さやページ遷移の重さに悩まされてきました。

しかし、htmx(Hypertext Markup Language eXtended)の登場により、この課題は劇的に改善されました。htmx を使えば、JavaScript を最小限に抑えながら、スムーズで直感的なウィザードフォームを実装できるのです。

この記事では、htmx を使った多段階ウィザードフォームの実装コツを、実際のコード例と共に詳しく解説していきます。初心者の方でも理解しやすいよう、段階的に説明していきましょう。

htmx の基本概念とウィザードフォーム

htmx は、HTML に動的な機能を追加するライブラリです。従来の JavaScript フレームワークとは異なり、サーバーサイドの HTML レスポンスを直接 DOM に反映させることで、シンプルで効率的なインタラクションを実現します。

htmx の基本属性

ウィザードフォームで使用する主要な htmx 属性を理解しましょう。

html<!-- 基本的なhtmx属性の例 -->
<div
  hx-post="/api/step1"
  hx-target="#wizard-content"
  hx-swap="innerHTML"
>
  <button type="submit">次へ進む</button>
</div>

このコードでは、hx-postで POST リクエストを送信し、hx-targetで更新対象を指定し、hx-swapで更新方法を定義しています。

ウィザードフォームとの相性

htmx は特にウィザードフォームと相性が良い理由があります:

  • 状態管理の簡素化: サーバーサイドで状態を管理できる
  • SEO フレンドリー: 各段階が独立した URL として扱える
  • プログレッシブエンハンスメント: JavaScript が無効でも基本機能が動作する

多段階ウィザードの設計思想

多段階ウィザードフォームを設計する際は、以下の 3 つの原則を押さえることが重要です。

1. 段階の独立性

各段階は独立した機能を持つべきです。これにより、保守性と再利用性が向上します。

html<!-- 段階ごとに独立したフォーム -->
<form hx-post="/wizard/step1" hx-target="#wizard-container">
  <div class="step-content">
    <h3>基本情報入力</h3>
    <input
      type="text"
      name="name"
      placeholder="お名前"
      required
    />
    <input
      type="email"
      name="email"
      placeholder="メールアドレス"
      required
    />
  </div>
  <div class="step-actions">
    <button type="submit">次へ進む</button>
  </div>
</form>

2. データの永続性

段階間でデータを失わないよう、適切な保存戦略を立てます。

html<!-- 隠しフィールドでデータを保持 -->
<form hx-post="/wizard/step2" hx-target="#wizard-container">
  <input
    type="hidden"
    name="step1_data"
    value="{{ step1_data }}"
  />
  <div class="step-content">
    <h3>詳細情報入力</h3>
    <input
      type="text"
      name="company"
      placeholder="会社名"
    />
    <input type="text" name="position" placeholder="役職" />
  </div>
  <div class="step-actions">
    <button type="button" hx-get="/wizard/step1">
      戻る
    </button>
    <button type="submit">次へ進む</button>
  </div>
</form>

3. ユーザビリティの最優先

ユーザーが迷わないよう、現在位置と進捗を明確に示します。

段階管理の実装方法

段階管理は、ウィザードフォームの核となる機能です。htmx を使った実装方法を見ていきましょう。

基本的な段階管理システム

まず、段階を管理する基本的な HTML 構造を作成します。

html<!-- ウィザードの基本構造 -->
<div id="wizard-container" class="wizard">
  <div class="wizard-progress">
    <div class="step active" data-step="1">基本情報</div>
    <div class="step" data-step="2">詳細情報</div>
    <div class="step" data-step="3">確認</div>
    <div class="step" data-step="4">完了</div>
  </div>

  <div id="wizard-content">
    <!-- 各段階のコンテンツがここに動的に挿入される -->
  </div>
</div>

段階遷移の実装

段階間の遷移を制御する JavaScript コードです。

javascript// 段階遷移の制御
document.addEventListener(
  'htmx:afterRequest',
  function (event) {
    const currentStep = event.detail.xhr.getResponseHeader(
      'X-Current-Step'
    );
    const totalSteps =
      event.detail.xhr.getResponseHeader('X-Total-Steps');

    if (currentStep && totalSteps) {
      updateProgress(
        parseInt(currentStep),
        parseInt(totalSteps)
      );
    }
  }
);

function updateProgress(current, total) {
  // プログレスバーの更新
  const progressBar = document.querySelector(
    '.wizard-progress'
  );
  const steps = progressBar.querySelectorAll('.step');

  steps.forEach((step, index) => {
    const stepNumber = index + 1;
    step.classList.remove('active', 'completed');

    if (stepNumber < current) {
      step.classList.add('completed');
    } else if (stepNumber === current) {
      step.classList.add('active');
    }
  });
}

サーバーサイドでの段階管理

Node.js/Express での段階管理の実装例です。

javascript// Express.jsでの段階管理
app.post('/wizard/step1', (req, res) => {
  const { name, email } = req.body;

  // バリデーション
  if (!name || !email) {
    return res.status(400).send(`
      <div class="error-message">
        <p>必須項目を入力してください。</p>
        <form hx-post="/wizard/step1" hx-target="#wizard-content">
          <input type="text" name="name" value="${
            name || ''
          }" placeholder="お名前" required>
          <input type="email" name="email" value="${
            email || ''
          }" placeholder="メールアドレス" required>
          <button type="submit">次へ進む</button>
        </form>
      </div>
    `);
  }

  // セッションにデータを保存
  req.session.wizardData = {
    ...req.session.wizardData,
    name,
    email,
  };

  // 次の段階のHTMLを返す
  res.setHeader('X-Current-Step', '2');
  res.setHeader('X-Total-Steps', '4');
  res.send(renderStep2());
});

状態保持のテクニック

ウィザードフォームでは、段階間でデータを失わないことが重要です。htmx での状態保持のテクニックを紹介します。

セッション管理による状態保持

サーバーサイドでセッションを使って状態を管理する方法です。

javascript// セッション管理の設定
const session = require('express-session');

app.use(
  session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: true,
    cookie: { secure: false }, // 本番環境ではtrueに設定
  })
);

// 段階データの保存
app.post('/wizard/step2', (req, res) => {
  const { company, position } = req.body;

  // 既存データと新しいデータをマージ
  req.session.wizardData = {
    ...req.session.wizardData,
    company,
    position,
  };

  // 次の段階へ
  res.setHeader('X-Current-Step', '3');
  res.send(renderStep3(req.session.wizardData));
});

クライアントサイドでの状態保持

htmx のhx-vals属性を使って、クライアントサイドでも状態を保持できます。

html<!-- クライアントサイドでの状態保持 -->
<form
  hx-post="/wizard/step3"
  hx-target="#wizard-content"
  hx-vals='{"step1_data": "{{ step1_data }}", "step2_data": "{{ step2_data }}"}'
>
  <div class="step-content">
    <h3>確認画面</h3>
    <div class="confirmation-data">
      <p><strong>お名前:</strong> {{ name }}</p>
      <p><strong>メールアドレス:</strong> {{ email }}</p>
      <p><strong>会社名:</strong> {{ company }}</p>
      <p><strong>役職:</strong> {{ position }}</p>
    </div>
  </div>

  <div class="step-actions">
    <button type="button" hx-get="/wizard/step2">
      戻る
    </button>
    <button type="submit">送信する</button>
  </div>
</form>

エラー時の状態復元

エラーが発生した際に、入力済みデータを復元する仕組みも重要です。

javascript// エラー時の状態復元
app.post('/wizard/step1', (req, res) => {
  const { name, email } = req.body;

  // バリデーションエラー
  if (!name || !email) {
    const savedData = req.session.wizardData || {};

    return res.status(400).send(`
      <div class="error-message">
        <p>必須項目を入力してください。</p>
      </div>
      <form hx-post="/wizard/step1" hx-target="#wizard-content">
        <input type="text" name="name" value="${
          savedData.name || ''
        }" placeholder="お名前" required>
        <input type="email" name="email" value="${
          savedData.email || ''
        }" placeholder="メールアドレス" required>
        <button type="submit">次へ進む</button>
      </form>
    `);
  }

  // 正常処理...
});

バリデーションとエラーハンドリング

適切なバリデーションとエラーハンドリングは、ユーザー体験を大きく左右します。

リアルタイムバリデーション

htmx のhx-trigger属性を使って、リアルタイムバリデーションを実装できます。

html<!-- リアルタイムバリデーション -->
<form hx-post="/wizard/step1" hx-target="#wizard-content">
  <div class="form-group">
    <label for="email">メールアドレス</label>
    <input
      type="email"
      id="email"
      name="email"
      hx-post="/validate/email"
      hx-trigger="blur"
      hx-target="#email-error"
      hx-swap="innerHTML"
    />
    <div id="email-error"></div>
  </div>

  <button type="submit">次へ進む</button>
</form>

サーバーサイドバリデーション

サーバーサイドでの包括的なバリデーション処理です。

javascript// サーバーサイドバリデーション
app.post('/wizard/step1', (req, res) => {
  const { name, email } = req.body;
  const errors = [];

  // 名前のバリデーション
  if (!name || name.trim().length === 0) {
    errors.push('お名前は必須です');
  } else if (name.length > 50) {
    errors.push('お名前は50文字以内で入力してください');
  }

  // メールアドレスのバリデーション
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!email || !emailRegex.test(email)) {
    errors.push('有効なメールアドレスを入力してください');
  }

  // エラーがある場合
  if (errors.length > 0) {
    return res.status(400).send(`
      <div class="error-summary">
        <h4>入力内容に問題があります</h4>
        <ul>
          ${errors
            .map((error) => `<li>${error}</li>`)
            .join('')}
        </ul>
      </div>
      <form hx-post="/wizard/step1" hx-target="#wizard-content">
        <input type="text" name="name" value="${
          name || ''
        }" placeholder="お名前" required>
        <input type="email" name="email" value="${
          email || ''
        }" placeholder="メールアドレス" required>
        <button type="submit">次へ進む</button>
      </form>
    `);
  }

  // 正常処理...
});

エラーメッセージの表示

ユーザーフレンドリーなエラーメッセージの表示方法です。

html<!-- エラーメッセージの表示 -->
<div class="form-container">
  <div id="error-messages" class="error-container"></div>

  <form
    hx-post="/wizard/step1"
    hx-target="#wizard-content"
    hx-swap="innerHTML"
  >
    <div class="form-group">
      <label for="name">お名前 *</label>
      <input type="text" id="name" name="name" required />
      <div class="field-error" id="name-error"></div>
    </div>

    <div class="form-group">
      <label for="email">メールアドレス *</label>
      <input
        type="email"
        id="email"
        name="email"
        required
      />
      <div class="field-error" id="email-error"></div>
    </div>

    <button type="submit">次へ進む</button>
  </form>
</div>

プログレス表示の実装

ユーザーが現在の位置を把握できるよう、プログレス表示を実装しましょう。

プログレスバーの実装

視覚的に分かりやすいプログレスバーを作成します。

html<!-- プログレスバーのHTML -->
<div class="wizard-progress">
  <div class="progress-bar">
    <div class="progress-fill" id="progress-fill"></div>
  </div>

  <div class="step-indicators">
    <div class="step active" data-step="1">
      <div class="step-number">1</div>
      <div class="step-label">基本情報</div>
    </div>
    <div class="step" data-step="2">
      <div class="step-number">2</div>
      <div class="step-label">詳細情報</div>
    </div>
    <div class="step" data-step="3">
      <div class="step-number">3</div>
      <div class="step-label">確認</div>
    </div>
    <div class="step" data-step="4">
      <div class="step-number">4</div>
      <div class="step-label">完了</div>
    </div>
  </div>
</div>

プログレス更新の JavaScript

段階が変わるたびにプログレスを更新する処理です。

javascript// プログレス更新の処理
function updateWizardProgress(currentStep, totalSteps) {
  // プログレスバーの更新
  const progressFill =
    document.getElementById('progress-fill');
  const percentage = (currentStep / totalSteps) * 100;
  progressFill.style.width = `${percentage}%`;

  // ステップインジケーターの更新
  const steps = document.querySelectorAll('.step');
  steps.forEach((step, index) => {
    const stepNumber = index + 1;
    step.classList.remove('active', 'completed');

    if (stepNumber < currentStep) {
      step.classList.add('completed');
    } else if (stepNumber === currentStep) {
      step.classList.add('active');
    }
  });
}

// htmxイベントでプログレスを更新
document.addEventListener(
  'htmx:afterRequest',
  function (event) {
    const currentStep = event.detail.xhr.getResponseHeader(
      'X-Current-Step'
    );
    const totalSteps =
      event.detail.xhr.getResponseHeader('X-Total-Steps');

    if (currentStep && totalSteps) {
      updateWizardProgress(
        parseInt(currentStep),
        parseInt(totalSteps)
      );
    }
  }
);

プログレスバーのスタイル

見た目を整える CSS スタイルです。

css/* プログレスバーのスタイル */
.wizard-progress {
  margin-bottom: 2rem;
}

.progress-bar {
  width: 100%;
  height: 8px;
  background-color: #e0e0e0;
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 1rem;
}

.progress-fill {
  height: 100%;
  background-color: #007bff;
  transition: width 0.3s ease;
}

.step-indicators {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.step {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 1;
}

.step-number {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background-color: #e0e0e0;
  color: #666;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  margin-bottom: 0.5rem;
  transition: all 0.3s ease;
}

.step.active .step-number {
  background-color: #007bff;
  color: white;
}

.step.completed .step-number {
  background-color: #28a745;
  color: white;
}

.step-label {
  font-size: 0.875rem;
  color: #666;
  text-align: center;
}

.step.active .step-label {
  color: #007bff;
  font-weight: bold;
}

レスポンシブ対応のポイント

モバイルデバイスでも使いやすいウィザードフォームにするためのポイントを紹介します。

モバイルファーストのレイアウト

小さな画面でも見やすいレイアウトを設計します。

html<!-- レスポンシブ対応のウィザード -->
<div class="wizard-container">
  <!-- モバイル用のプログレス表示 -->
  <div class="mobile-progress">
    <div class="progress-text">
      ステップ <span id="current-step">1</span> /
      <span id="total-steps">4</span>
    </div>
    <div class="progress-bar">
      <div
        class="progress-fill"
        id="mobile-progress-fill"
      ></div>
    </div>
  </div>

  <!-- デスクトップ用のプログレス表示 -->
  <div class="desktop-progress">
    <!-- 既存のプログレスバー -->
  </div>

  <div id="wizard-content" class="wizard-content">
    <!-- ウィザードのコンテンツ -->
  </div>
</div>

レスポンシブ CSS

画面サイズに応じて表示を調整する CSS です。

css/* レスポンシブ対応のCSS */
.wizard-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 1rem;
}

/* モバイル用プログレス(デフォルトで表示) */
.mobile-progress {
  display: block;
  margin-bottom: 1rem;
}

.desktop-progress {
  display: none;
}

.progress-text {
  text-align: center;
  margin-bottom: 0.5rem;
  font-weight: bold;
  color: #333;
}

/* タブレット以上でデスクトップ用プログレスを表示 */
@media (min-width: 768px) {
  .mobile-progress {
    display: none;
  }

  .desktop-progress {
    display: block;
  }

  .wizard-container {
    padding: 2rem;
  }
}

/* フォーム要素のレスポンシブ対応 */
.form-group {
  margin-bottom: 1rem;
}

.form-group label {
  display: block;
  margin-bottom: 0.5rem;
  font-weight: bold;
}

.form-group input,
.form-group select,
.form-group textarea {
  width: 100%;
  padding: 0.75rem;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 1rem;
}

/* ボタンのレスポンシブ対応 */
.step-actions {
  display: flex;
  gap: 1rem;
  justify-content: space-between;
  margin-top: 2rem;
}

.step-actions button {
  flex: 1;
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 4px;
  font-size: 1rem;
  cursor: pointer;
  transition: background-color 0.3s ease;
}

.step-actions .btn-primary {
  background-color: #007bff;
  color: white;
}

.step-actions .btn-secondary {
  background-color: #6c757d;
  color: white;
}

/* モバイルではボタンを縦並びに */
@media (max-width: 480px) {
  .step-actions {
    flex-direction: column;
  }

  .step-actions button {
    margin-bottom: 0.5rem;
  }
}

タッチフレンドリーなインターフェース

モバイルデバイスでの操作性を向上させる工夫です。

css/* タッチフレンドリーなスタイル */
@media (max-width: 768px) {
  /* タッチターゲットの最小サイズ */
  button,
  input[type='submit'],
  input[type='button'] {
    min-height: 44px;
    min-width: 44px;
  }

  /* フォーム要素のタッチ最適化 */
  input,
  select,
  textarea {
    font-size: 16px; /* iOSでズームを防ぐ */
    padding: 12px;
  }

  /* チェックボックスとラジオボタンの拡大 */
  input[type='checkbox'],
  input[type='radio'] {
    transform: scale(1.2);
    margin-right: 8px;
  }
}

アクセシビリティの考慮

すべてのユーザーが使いやすいウィザードフォームにするためのアクセシビリティ対応です。

ARIA 属性の活用

スクリーンリーダーなどの支援技術に対応する ARIA 属性を追加します。

html<!-- アクセシビリティ対応のウィザード -->
<div
  class="wizard"
  role="application"
  aria-label="多段階フォーム"
>
  <div
    class="wizard-progress"
    role="progressbar"
    aria-valuenow="1"
    aria-valuemin="1"
    aria-valuemax="4"
  >
    <div class="progress-bar">
      <div class="progress-fill" id="progress-fill"></div>
    </div>

    <div class="step-indicators" role="tablist">
      <div
        class="step active"
        role="tab"
        aria-selected="true"
        aria-label="ステップ1: 基本情報"
      >
        <div class="step-number" aria-hidden="true">1</div>
        <div class="step-label">基本情報</div>
      </div>
      <div
        class="step"
        role="tab"
        aria-selected="false"
        aria-label="ステップ2: 詳細情報"
      >
        <div class="step-number" aria-hidden="true">2</div>
        <div class="step-label">詳細情報</div>
      </div>
      <!-- 他のステップ... -->
    </div>
  </div>

  <div
    id="wizard-content"
    role="tabpanel"
    aria-labelledby="current-step"
  >
    <!-- ウィザードのコンテンツ -->
  </div>
</div>

キーボードナビゲーション

キーボードだけで操作できるようにします。

javascript// キーボードナビゲーションの実装
document.addEventListener('keydown', function (event) {
  const currentStep =
    document.querySelector('.step.active');
  const steps = document.querySelectorAll('.step');
  const currentIndex =
    Array.from(steps).indexOf(currentStep);

  switch (event.key) {
    case 'ArrowLeft':
      if (currentIndex > 0) {
        event.preventDefault();
        navigateToStep(currentIndex);
      }
      break;

    case 'ArrowRight':
      if (currentIndex < steps.length - 1) {
        event.preventDefault();
        navigateToStep(currentIndex + 2);
      }
      break;

    case 'Enter':
      // フォーム送信
      const form = document.querySelector(
        '#wizard-content form'
      );
      if (form) {
        form.requestSubmit();
      }
      break;
  }
});

function navigateToStep(stepNumber) {
  // 段階遷移の処理
  htmx.ajax('GET', `/wizard/step${stepNumber}`, {
    target: '#wizard-content',
    swap: 'innerHTML',
  });
}

エラー通知の改善

エラーメッセージを適切に通知する仕組みです。

html<!-- アクセシビリティ対応のエラー表示 -->
<div class="form-container">
  <div
    id="error-summary"
    class="error-summary"
    role="alert"
    aria-live="polite"
    aria-atomic="true"
  ></div>

  <form
    hx-post="/wizard/step1"
    hx-target="#wizard-content"
    hx-swap="innerHTML"
  >
    <div class="form-group">
      <label for="name" id="name-label">お名前 *</label>
      <input
        type="text"
        id="name"
        name="name"
        aria-labelledby="name-label"
        aria-describedby="name-error"
        aria-invalid="false"
        required
      />
      <div
        class="field-error"
        id="name-error"
        role="alert"
        aria-live="polite"
      ></div>
    </div>

    <button type="submit" aria-describedby="submit-help">
      次へ進む
    </button>
    <div id="submit-help" class="sr-only">
      フォームを送信して次の段階に進みます
    </div>
  </form>
</div>

スクリーンリーダー用のスタイル

視覚的に隠しつつ、スクリーンリーダーには読み上げられる要素のスタイルです。

css/* スクリーンリーダー専用のスタイル */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* フォーカス表示の改善 */
button:focus,
input:focus,
select:focus,
textarea:focus {
  outline: 2px solid #007bff;
  outline-offset: 2px;
}

/* エラー状態の視覚的表示 */
input[aria-invalid='true'] {
  border-color: #dc3545;
  box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}

.field-error {
  color: #dc3545;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

パフォーマンス最適化

htmx ウィザードフォームのパフォーマンスを最適化するテクニックを紹介します。

キャッシュ戦略

適切なキャッシュ設定でパフォーマンスを向上させます。

javascript// キャッシュ戦略の実装
app.get('/wizard/step1', (req, res) => {
  // 静的コンテンツのキャッシュ設定
  res.setHeader('Cache-Control', 'public, max-age=300'); // 5分間キャッシュ

  const html = renderStep1();
  res.send(html);
});

app.post('/wizard/step1', (req, res) => {
  // 動的コンテンツはキャッシュしない
  res.setHeader(
    'Cache-Control',
    'no-cache, no-store, must-revalidate'
  );
  res.setHeader('Pragma', 'no-cache');
  res.setHeader('Expires', '0');

  // 処理...
});

遅延読み込み

必要に応じてコンテンツを遅延読み込みします。

html<!-- 遅延読み込みの実装 -->
<div class="wizard-content">
  <div id="step1-content" class="step-content active">
    <!-- 最初の段階のコンテンツ -->
  </div>

  <div
    id="step2-content"
    class="step-content"
    hx-get="/wizard/step2"
    hx-trigger="load once"
  >
    <div class="loading">読み込み中...</div>
  </div>

  <div
    id="step3-content"
    class="step-content"
    hx-get="/wizard/step3"
    hx-trigger="load once"
  >
    <div class="loading">読み込み中...</div>
  </div>
</div>

画像の最適化

画像の読み込みを最適化します。

html<!-- 画像の最適化 -->
<div class="wizard-step">
  <img
    src="/images/placeholder.jpg"
    data-src="/images/actual-image.jpg"
    class="lazy-image"
    alt="説明画像"
    loading="lazy"
  />
</div>
javascript// 遅延読み込みのJavaScript
document.addEventListener('DOMContentLoaded', function () {
  const lazyImages =
    document.querySelectorAll('.lazy-image');

  const imageObserver = new IntersectionObserver(
    (entries, observer) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.classList.remove('lazy-image');
          observer.unobserve(img);
        }
      });
    }
  );

  lazyImages.forEach((img) => imageObserver.observe(img));
});

バンドルサイズの最適化

htmx の読み込みを最適化します。

html<!-- htmxの最適化された読み込み -->
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>ウィザードフォーム</title>

    <!-- htmxをCDNから読み込み -->
    <script
      src="https://unpkg.com/htmx.org@1.9.10"
      integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
      crossorigin="anonymous"
    ></script>

    <!-- プリロードでパフォーマンス向上 -->
    <link
      rel="preload"
      href="/wizard/step2"
      as="fetch"
      crossorigin
    />
    <link
      rel="preload"
      href="/wizard/step3"
      as="fetch"
      crossorigin
    />
  </head>
  <body>
    <!-- ウィザードのコンテンツ -->
  </body>
</html>

まとめ

htmx を使った多段階ウィザードフォームの実装は、従来の JavaScript フレームワークとは異なるアプローチを提供します。サーバーサイドの力を最大限活用しながら、シンプルで保守性の高いコードを書けるのが大きな魅力です。

今回紹介したテクニックを活用することで、以下のようなメリットが得られます:

  • 開発効率の向上: 複雑な状態管理が不要
  • SEO フレンドリー: 各段階が独立した URL として扱える
  • アクセシビリティ: プログレッシブエンハンスメントの実現
  • パフォーマンス: 軽量で高速な動作

特に、段階管理の実装方法や状態保持のテクニックは、他のプロジェクトでも応用できる汎用的な知識です。バリデーションとエラーハンドリングの実装は、ユーザー体験を大きく左右する重要な要素ですので、しっかりと理解しておきましょう。

htmx の真の価値は、Web の本来の仕組みを活かしながら、モダンなインタラクションを実現できる点にあります。この記事で紹介したコツを参考に、あなただけの素晴らしいウィザードフォームを作成してください。

関連リンク