「広告費をかけてLPに集客しているのに、フォームで離脱されてしまう」。Web担当者なら一度はぶつかる壁ではないでしょうか。

実は、フォーム離脱の原因の多くは「聞いている内容が多すぎる」「デザインが悪い」といったUI面だけではありません。バリデーションのタイミングエラーメッセージの出し方送信ボタンを押した後の挙動といった、フロントエンド実装の細部がユーザーのストレスを生んでいるケースが非常に多いのです。

この記事では、LPのコンバージョン率に直結するフォーム実装の最適化パターンを、具体的なコード例とともに解説します。UI/UXの設計原則についてはUXデザインラボの解説記事も併せてご覧ください。

フォーム離脱はどこで起きているのか

Googleの調査によると、モバイルユーザーの約67%がフォーム入力中に離脱するとされています。特に多い離脱ポイントは以下の3つです。

  • 入力途中のバリデーションエラーが分かりにくく、修正方法が伝わらない
  • 送信ボタンを押した後にページがリロードされ、入力内容が消える
  • エラーがページ上部にまとめて表示され、どの項目を直せばいいか分からない

いずれも、フォームの「設計」ではなく「実装」の問題です。つまり、開発者の手で直接改善できる領域になります。

HTML5 Constraint Validation APIを使いこなす

最初に取り組むべきは、ブラウザが標準で提供しているConstraint Validation APIの活用です。多くの開発者がこのAPIの存在を知りながら、十分に活用できていません。

基本的なバリデーション属性

HTMLの入力要素には、JavaScriptを書かなくてもバリデーションを実現できる属性が用意されています。

<form novalidate>
  <label for="email">メールアドレス</label>
  <input
    type="email"
    id="email"
    name="email"
    required
    pattern="[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}"
    aria-describedby="email-error"
  />
  <span id="email-error" role="alert" aria-live="polite"></span>
</form>

ここでのポイントは novalidate 属性です。フォーム要素に novalidate を付けることで、ブラウザのデフォルトバルーンを無効化しつつ、Constraint Validation API自体は引き続き利用できます。これにより、ブラウザ間で統一されたカスタムエラーUIを実装できるようになります。

詳しくはMDN Web Docsの「制約の検証」にAPIの全体像がまとまっています。

カスタムバリデーションメッセージの設定

setCustomValidity() メソッドを使うと、日本語のエラーメッセージを設定できます。

const emailInput = document.getElementById('email');

emailInput.addEventListener('invalid', (e) => {
  if (e.target.validity.valueMissing) {
    e.target.setCustomValidity('メールアドレスを入力してください');
  } else if (e.target.validity.typeMismatch || e.target.validity.patternMismatch) {
    e.target.setCustomValidity('正しいメールアドレスの形式で入力してください');
  }
});

emailInput.addEventListener('input', (e) => {
  e.target.setCustomValidity('');
});

validity オブジェクトの各プロパティ(valueMissingtypeMismatchpatternMismatch など)を参照することで、エラーの種類に応じた的確なメッセージを出し分けられます。

リアルタイムバリデーションの実装で気をつけること

blurイベントで検証するのが基本

入力中にリアルタイムでエラーを出す実装を見かけますが、これは多くの場合ユーザー体験を損ないます。「メールアドレスを入力し始めた瞬間に『形式が正しくありません』と赤字が出る」という状況を想像してみてください。

web.devのフォームベストプラクティスガイドでも推奨されているように、バリデーションの発火タイミングはblur(入力欄からフォーカスが外れたとき)が適切です。

const formFields = document.querySelectorAll('input, select, textarea');

formFields.forEach((field) => {
  field.addEventListener('blur', () => {
    validateField(field);
  });

  // 一度エラーが出た項目は、修正中にリアルタイムで再検証する
  field.addEventListener('input', () => {
    if (field.classList.contains('is-invalid')) {
      validateField(field);
    }
  });
});

この実装のポイントは、最初のバリデーションはblurで行い、エラー状態になった後の再検証はinputイベントで行うという2段階のアプローチです。ユーザーが修正を始めたらすぐにエラーが消えるので、「ちゃんと直せている」という安心感を与えられます。

エラーメッセージは入力欄の直下に表示する

エラーメッセージの表示位置も重要です。ページ上部にまとめて表示するパターンは、特に項目数が多いフォームでは「どこを直せばいいのか」がわかりにくくなります。

function validateField(field) {
  const errorElement = document.getElementById(field.id + '-error');

  if (!field.checkValidity()) {
    field.classList.add('is-invalid');
    field.classList.remove('is-valid');
    errorElement.textContent = getErrorMessage(field);
    field.setAttribute('aria-invalid', 'true');
  } else {
    field.classList.remove('is-invalid');
    field.classList.add('is-valid');
    errorElement.textContent = '';
    field.setAttribute('aria-invalid', 'false');
  }
}

aria-invalid 属性と role="alert" を組み合わせることで、スクリーンリーダーを使用しているユーザーにもエラー状態が適切に伝わります。アクセシビリティの詳細はW3C WAI のフォームチュートリアルが網羅的なリファレンスになっています。

ステップフォームで入力負荷を分散する

項目数が5つを超えるフォームでは、ステップフォーム(段階的開示)の導入を検討してください。1画面に全項目を表示するとユーザーに心理的な負荷がかかりますが、2〜3ステップに分割するだけで完了率が大きく改善するケースがあります。

class StepForm {
  constructor(formElement) {
    this.form = formElement;
    this.steps = Array.from(formElement.querySelectorAll('[data-step]'));
    this.currentStep = 0;
    this.init();
  }

  init() {
    this.showStep(0);
    this.updateProgress();
  }

  showStep(index) {
    this.steps.forEach((step, i) => {
      step.hidden = i !== index;
      step.setAttribute('aria-hidden', i !== index);
    });
    this.currentStep = index;
    const firstInput = this.steps[index].querySelector('input, select, textarea');
    if (firstInput) firstInput.focus();
  }

  async nextStep() {
    const currentFields = this.steps[this.currentStep]
      .querySelectorAll('input, select, textarea');

    let isValid = true;
    currentFields.forEach((field) => {
      if (!field.checkValidity()) {
        validateField(field);
        isValid = false;
      }
    });

    if (isValid && this.currentStep < this.steps.length - 1) {
      this.showStep(this.currentStep + 1);
      this.updateProgress();
    }
  }

  updateProgress() {
    const progress = ((this.currentStep + 1) / this.steps.length) * 100;
    const bar = this.form.querySelector('.progress-bar');
    if (bar) bar.style.width = progress + '%';
  }
}

ステップ間の遷移では、現在のステップの入力内容だけを検証してから次に進む設計にします。これにより「全部入力したのに最初の項目でエラーが出て戻される」という不満を防げます。

なお、モバイルファーストのLP設計テクニックについてはUXデザインラボの記事で、ステップフォームのデザイン面のポイントが詳しく解説されています。

送信処理はfetch APIで非同期に

フォーム送信でページ全体がリロードされると、万が一サーバーエラーが返った場合に入力内容がすべて失われます。これはユーザーにとって致命的な体験です。

async function handleSubmit(form) {
  const submitButton = form.querySelector('[type="submit"]');
  const originalText = submitButton.textContent;

  // 二重送信防止
  submitButton.disabled = true;
  submitButton.textContent = '送信中...';

  try {
    const formData = new FormData(form);
    const response = await fetch(form.action, {
      method: 'POST',
      body: formData,
    });

    if (!response.ok) {
      throw new Error('送信に失敗しました');
    }

    // 成功時のサンクスメッセージを表示
    const successDiv = document.createElement('div');
    successDiv.className = 'success-message';
    successDiv.setAttribute('role', 'status');
    const h2 = document.createElement('h2');
    h2.textContent = 'お問い合わせありがとうございます';
    const p = document.createElement('p');
    p.textContent = '内容を確認のうえ、2営業日以内にご連絡いたします。';
    successDiv.appendChild(h2);
    successDiv.appendChild(p);
    form.replaceWith(successDiv);
  } catch (error) {
    // エラー時は入力内容を保持したままエラーを表示
    const errorArea = form.querySelector('.form-error-area');
    errorArea.textContent = '送信に失敗しました。時間をおいて再度お試しください。';
    errorArea.setAttribute('role', 'alert');
    submitButton.disabled = false;
    submitButton.textContent = originalText;
  }
}

この実装で重要なのは、エラー時に入力内容を消さないことと、送信ボタンのdisabled制御で二重送信を防ぐことの2点です。成功時にはフォーム全体をサンクスメッセージに置き換えることで、ページ遷移なしにコンバージョン完了を伝えられます。

送信イベントの計測を実装に組み込む

フォームの改善効果を定量的に把握するには、各ステップのイベント計測が欠かせません。以下のようなポイントでイベントを発火させます。

function trackFormEvent(eventName, params) {
  if (typeof gtag === 'function') {
    gtag('event', eventName, Object.assign({
      event_category: 'lp_form'
    }, params || {}));
  }
}

// フォーム表示時
trackFormEvent('form_view');

// 最初の項目にフォーカスしたとき(入力開始)
firstField.addEventListener('focus', function() {
  trackFormEvent('form_start');
}, { once: true });

// 各ステップ完了時
trackFormEvent('step_complete', { step: currentStep + 1 });

// 送信成功時
trackFormEvent('form_submit_success');

GA4でのLP効果測定の詳しいイベント設計については、データインサイトラボの解説記事が参考になります。どの項目で離脱が多いかをデータで把握できれば、改善の優先順位が明確になります。

パフォーマンスへの配慮も忘れずに

フォームのバリデーションライブラリやUIコンポーネントを安易に追加すると、JavaScriptのバンドルサイズが膨らみ、ページの初期表示が遅くなります。

実際のところ、この記事で紹介したパターンのほとんどはバニラJavaScript(ライブラリなし)で実装できます。Constraint Validation APIはモダンブラウザで広くサポートされており、MDN Web Docsのブラウザ互換性テーブルで確認できる通り、主要ブラウザで問題なく動作します。

LPの表示速度全般の最適化については、テックビルドのCore Web Vitals対応記事で詳しく解説していますので、フォーム実装と合わせて取り組むことをおすすめします。

まとめ

LPのフォーム実装は、広告やデザインと比べて地味に見えるかもしれません。しかし、ユーザーが「申し込もう」と決めた後の最後の関門がフォームです。ここでの離脱を防ぐことは、コンバージョン率への影響が非常に大きい施策になります。

今回紹介したパターンをすべて一度に導入する必要はありません。まずは来週、blurイベントでのリアルタイムバリデーションを1つのフォームに実装してみてください。エラーメッセージを入力欄の直下に表示し、aria-invalid 属性を付けるだけでも、ユーザー体験は目に見えて変わります。

その効果をGA4のイベントで計測し、数値で改善を確認できたら、ステップフォームや非同期送信にも段階的に取り組んでいきましょう。