【JavaScript】フォームバリデーション完全ガイド|リアルタイム検証・正規表現パターン・再利用可能設計・アクセシビリティ対応まで解説

フォームバリデーションは「ユーザーが正しい形式でデータを入力しているか」をチェックする処理です。送信後のサーバーエラーを減らし、ユーザーに即座にフィードバックを与えることでUXが大幅に向上します。

この記事では、HTML5 属性だけでは対応できないカスタムバリデーションの実装を中心に、よく使う正規表現パターン集、リアルタイム検証、再利用可能な設計、スクリーンリーダー対応のアクセシビリティまで体系的に解説します。

スポンサーリンク

HTML5 属性バリデーション vs JavaScript バリデーション

まず「どちらを使うべきか」を整理します。答えは両方を組み合わせることです。

HTML5 属性 JavaScript
設定の手軽さ ◎ 属性1つで動く △ コードが必要
エラーメッセージのカスタマイズ △ ブラウザ依存で見た目が変わる ◎ 完全に制御できる
リアルタイム検証 × 基本は送信時のみ ◎ blur/input で随時検証可
複数フィールドの依存チェック × 単一フィールドのみ ◎ 任意のロジックが書ける
アクセシビリティ ○ ブラウザが処理 ◎ aria-invalid 等で細かく制御
推奨パターン:HTML5 の requiredtype="email"minlength 等で基本チェックを行い、カスタムメッセージや複雑なルールは JavaScript で追加する二段構えが最も堅牢です。必ずサーバー側でも検証してください。クライアント側の検証は無効化できます。

エラー表示の基本設計

バリデーションの実装前に、エラーを表示する HTML・CSS の構造を統一しておきます。

HTML(フィールド構造)
<div class="field-group">
  <label for="email">メールアドレス <span class="required">*</span></label>
  <input
    type="email"
    id="email"
    name="email"
    aria-describedby="email-error"
    aria-invalid="false"
    autocomplete="email"
  >
  <span id="email-error" class="error-msg" role="alert"></span>
</div>
CSS(エラー表示スタイル)
.field-group { margin-bottom: 20px; }

.field-group label {
  display: block;
  margin-bottom: 4px;
  font-weight: bold;
}

.required { color: #e11d48; }

.field-group input {
  width: 100%;
  padding: 8px 12px;
  border: 2px solid #cbd5e1;
  border-radius: 6px;
  font-size: 1rem;
  transition: border-color 0.2s;
}

.field-group input:focus {
  outline: none;
  border-color: #0284c7;
}

/* エラー状態 */
.field-group input[aria-invalid="true"] {
  border-color: #e11d48;
}

.error-msg {
  display: block;
  margin-top: 4px;
  color: #e11d48;
  font-size: 0.85rem;
  min-height: 1.2em; /* 高さが跳ねないよう確保 */
}
aria-describedby・aria-invalid とは:aria-describedby="email-error" は「このinputの説明がid=”email-error”の要素にある」ことをスクリーンリーダーに伝えます。aria-invalid="true" にするとエラー状態であることを伝えられます。これにより視覚に頼らないユーザーもエラーの内容を把握できます。

よく使うバリデーションルール集

実務で頻繁に使うバリデーション関数をまとめました。そのままコピーして使えます。

バリデーションルール関数集
const rules = {
  // 必須チェック(空白のみも NG)
  required: (v) => v.trim() !== '' || '必須項目です',

  // メールアドレス
  email: (v) =>
    /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/.test(v.trim())
    || '有効なメールアドレスを入力してください',

  // 最小文字数
  minLength: (min) => (v) =>
    v.length >= min || `${min}文字以上で入力してください`,

  // 最大文字数
  maxLength: (max) => (v) =>
    v.length <= max || `${max}文字以内で入力してください`,

  // 数値のみ
  numeric: (v) =>
    /^\d+$/.test(v) || '半角数字のみ入力してください',

  // 電話番号(ハイフンあり・なし両対応)
  tel: (v) =>
    /^0\d{1,4}-?\d{1,4}-?\d{4}$/.test(v.replace(/\s/g, ''))
    || '有効な電話番号を入力してください',

  // 郵便番号(123-4567 または 1234567)
  postalCode: (v) =>
    /^\d{3}-?\d{4}$/.test(v) || '郵便番号は「123-4567」形式で入力してください',

  // パスワード強度(英大文字・小文字・数字を各1文字以上含む8文字以上)
  passwordStrong: (v) =>
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(v)
    || '8文字以上で大文字・小文字・数字を各1文字以上含めてください',

  // URL
  url: (v) => {
    try { new URL(v); return true; }
    catch { return '有効なURLを入力してください'; }
  },

  // カタカナのみ
  katakana: (v) =>
    /^[\u30A0-\u30FF\u30FB-\u30FC]+$/.test(v) || '全角カタカナで入力してください',

  // 数値範囲チェック
  range: (min, max) => (v) => {
    const n = Number(v);
    return (!isNaN(n) && n >= min && n <= max)
      || `${min}〜${max}の数値を入力してください`;
  },
};

// 使い方
const result = rules.email('test@example.com');
// true が返れば OK、文字列が返ればエラーメッセージ
if (result !== true) console.error(result);

フィールド単位のバリデーター関数

上記のルール関数を組み合わせて、フィールドを検証し DOM にエラーを反映する汎用関数を作ります。

validate 関数(フィールド単位)
/**
 * @param {HTMLInputElement} input - 対象の input 要素
 * @param {Function[]} ruleList   - 適用するルール関数の配列
 * @returns {boolean} 全ルール通過なら true
 */
function validate(input, ruleList) {
  const errorEl = document.getElementById(input.getAttribute('aria-describedby'));

  for (const rule of ruleList) {
    const result = rule(input.value);
    if (result !== true) {
      // エラー表示
      input.setAttribute('aria-invalid', 'true');
      if (errorEl) errorEl.textContent = result;
      return false;
    }
  }

  // エラー解除
  input.setAttribute('aria-invalid', 'false');
  if (errorEl) errorEl.textContent = '';
  return true;
}

リアルタイムバリデーション(blur + input)

入力中(input イベント)と入力完了(blur イベント)を使い分けることで、邪魔にならないリアルタイム検証を実装できます。

タイミング 使用イベント メリット
フォーカスが外れたとき blur 入力途中に余計なエラーが出ない
文字を入力するたび input 即座にフィードバック。エラー解除に使いやすい
送信ボタンを押したとき submit 全フィールドを一括検証
HTML(完全なフォーム例)
<form id="registerForm" novalidate>
  <div class="field-group">
    <label for="name">お名前 <span class="required">*</span></label>
    <input type="text" id="name" name="name"
      aria-describedby="name-error" aria-invalid="false"
      autocomplete="name">
    <span id="name-error" class="error-msg" role="alert"></span>
  </div>
  <div class="field-group">
    <label for="email">メールアドレス <span class="required">*</span></label>
    <input type="email" id="email" name="email"
      aria-describedby="email-error" aria-invalid="false"
      autocomplete="email">
    <span id="email-error" class="error-msg" role="alert"></span>
  </div>
  <div class="field-group">
    <label for="password">パスワード <span class="required">*</span></label>
    <input type="password" id="password" name="password"
      aria-describedby="password-error" aria-invalid="false"
      autocomplete="new-password">
    <span id="password-error" class="error-msg" role="alert"></span>
  </div>
  <div class="field-group">
    <label for="confirm">パスワード(確認) <span class="required">*</span></label>
    <input type="password" id="confirm" name="confirm"
      aria-describedby="confirm-error" aria-invalid="false"
      autocomplete="new-password">
    <span id="confirm-error" class="error-msg" role="alert"></span>
  </div>
  <button type="submit">登録する</button>
</form>
リアルタイム + 送信時バリデーション(完全実装)
// ───── ルール定義(再掲・抜粋)─────
const rules = {
  required:       (v) => v.trim() !== '' || '必須項目です',
  email:          (v) => /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/.test(v.trim()) || '有効なメールアドレスを入力してください',
  minLength: (n) => (v) => v.length >= n || `${n}文字以上で入力してください`,
  passwordStrong: (v) => /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(v) || '8文字以上・大文字/小文字/数字を各1文字以上含めてください',
};

// ───── フィールドとルールのマッピング ─────
const fields = [
  { id: 'name',     ruleList: [rules.required, rules.minLength(2)] },
  { id: 'email',    ruleList: [rules.required, rules.email] },
  { id: 'password', ruleList: [rules.required, rules.passwordStrong] },
];

// ───── バリデーター関数 ─────
function validate(input, ruleList) {
  const errorEl = document.getElementById(input.getAttribute('aria-describedby'));
  for (const rule of ruleList) {
    const result = rule(input.value);
    if (result !== true) {
      input.setAttribute('aria-invalid', 'true');
      if (errorEl) errorEl.textContent = result;
      return false;
    }
  }
  input.setAttribute('aria-invalid', 'false');
  if (errorEl) errorEl.textContent = '';
  return true;
}

// ───── リアルタイム検証のセットアップ ─────
fields.forEach(({ id, ruleList }) => {
  const input = document.getElementById(id);

  // blur: フォーカスが外れたとき → 検証開始
  input.addEventListener('blur', () => validate(input, ruleList));

  // input: 文字を入力するたびに → エラーが出ていれば即時解除チェック
  input.addEventListener('input', () => {
    if (input.getAttribute('aria-invalid') === 'true') {
      validate(input, ruleList);
    }
  });
});

// ───── 送信時:全フィールドを一括検証 ─────
const form = document.getElementById('registerForm');
form.addEventListener('submit', (e) => {
  e.preventDefault();

  const results = fields.map(({ id, ruleList }) =>
    validate(document.getElementById(id), ruleList)
  );

  // パスワード確認チェック(複数フィールド依存)
  const pwOk = validateConfirm();

  if (results.every(Boolean) && pwOk) {
    console.log('バリデーション通過!送信処理へ');
    // fetch('/api/register', { method: 'POST', body: new FormData(form) });
  } else {
    // 最初のエラーフィールドにフォーカスを移動
    const firstError = form.querySelector('[aria-invalid="true"]');
    firstError?.focus();
  }
});

フォーカスイベントの詳細は【JavaScript】フォーカスイベントの使い方も参考にしてください。

複数フィールドの依存チェック(パスワード確認・日付範囲など)

単一フィールドのルールでは対応できない「2つのフィールドを比較する」ケースです。

パスワード確認フィールド

パスワード確認バリデーション
function validateConfirm() {
  const pw      = document.getElementById('password');
  const confirm = document.getElementById('confirm');
  const errorEl = document.getElementById('confirm-error');

  if (confirm.value === '') {
    confirm.setAttribute('aria-invalid', 'true');
    errorEl.textContent = '必須項目です';
    return false;
  }
  if (pw.value !== confirm.value) {
    confirm.setAttribute('aria-invalid', 'true');
    errorEl.textContent = 'パスワードが一致しません';
    return false;
  }
  confirm.setAttribute('aria-invalid', 'false');
  errorEl.textContent = '';
  return true;
}

// パスワードが変更されたら確認フィールドも再検証
document.getElementById('password').addEventListener('input', () => {
  const confirm = document.getElementById('confirm');
  if (confirm.value !== '') validateConfirm();
});
document.getElementById('confirm').addEventListener('blur', validateConfirm);

日付の前後チェック(開始日 ≤ 終了日)

日付範囲バリデーション
function validateDateRange() {
  const start   = document.getElementById('start-date');
  const end     = document.getElementById('end-date');
  const errorEl = document.getElementById('end-date-error');

  if (start.value && end.value && end.value < start.value) {
    end.setAttribute('aria-invalid', 'true');
    errorEl.textContent = '終了日は開始日以降の日付を選択してください';
    return false;
  }
  end.setAttribute('aria-invalid', 'false');
  errorEl.textContent = '';
  return true;
}

パスワード強度メーター(視覚的フィードバック)

パスワード強度をリアルタイムで視覚的に伝えるUIはUX向上に効果的です。

HTML(強度メーター)
<div class="field-group">
  <label for="pw-meter">パスワード</label>
  <input type="password" id="pw-meter" aria-describedby="pw-strength-msg pw-meter-error">
  <div class="strength-bar">
    <div id="strength-fill" class="strength-fill"></div>
  </div>
  <span id="pw-strength-msg" class="strength-label"></span>
  <span id="pw-meter-error" class="error-msg" role="alert"></span>
</div>
CSS(強度バー)
.strength-bar {
  height: 6px;
  background: #e2e8f0;
  border-radius: 3px;
  margin-top: 6px;
  overflow: hidden;
}

.strength-fill {
  height: 100%;
  width: 0;
  border-radius: 3px;
  transition: width 0.3s, background 0.3s;
}
パスワード強度チェック
function getStrength(pw) {
  let score = 0;
  if (pw.length >= 8)  score++;
  if (pw.length >= 12) score++;
  if (/[a-z]/.test(pw) && /[A-Z]/.test(pw)) score++;
  if (/\d/.test(pw))   score++;
  if (/[^a-zA-Z\d]/.test(pw)) score++; // 記号
  return score; // 0〜5
}

const STRENGTH_LABELS = [
  { label: '',       color: '',        width: '0%' },
  { label: '弱い',   color: '#e11d48', width: '20%' },
  { label: '普通',   color: '#f59e0b', width: '50%' },
  { label: '強い',   color: '#10b981', width: '75%' },
  { label: 'とても強い', color: '#0284c7', width: '100%' },
  { label: '最高',   color: '#7c3aed', width: '100%' },
];

document.getElementById('pw-meter').addEventListener('input', (e) => {
  const score = Math.min(getStrength(e.target.value), STRENGTH_LABELS.length - 1);
  const { label, color, width } = STRENGTH_LABELS[score];
  const fill = document.getElementById('strength-fill');
  const msg  = document.getElementById('pw-strength-msg');
  fill.style.width  = e.target.value ? width : '0%';
  fill.style.background = color;
  msg.textContent   = e.target.value ? `強度:${label}` : '';
  msg.style.color   = color;
});

Constraint Validation API の活用

HTML5 には Constraint Validation API が組み込まれており、JavaScript から検証状態を操作できます。カスタムメッセージの設定に特に便利です。

setCustomValidity でメッセージを日本語化
const emailInput = document.getElementById('email');

emailInput.addEventListener('invalid', (e) => {
  e.preventDefault(); // ブラウザのデフォルトUIを抑制
  if (emailInput.validity.valueMissing) {
    emailInput.setCustomValidity('メールアドレスは必須です');
  } else if (emailInput.validity.typeMismatch) {
    emailInput.setCustomValidity('有効なメールアドレスを入力してください');
  }
});

// 入力のたびにカスタムメッセージをリセット(重要!)
emailInput.addEventListener('input', () => {
  emailInput.setCustomValidity('');
});

// validity オブジェクトの主なプロパティ
// validity.valueMissing  : required で空
// validity.typeMismatch  : type 属性の形式不一致
// validity.patternMismatch: pattern 属性の不一致
// validity.tooShort      : minlength より短い
// validity.rangeUnderflow: min より小さい
// validity.valid         : すべてのチェックを通過

novalidate と checkValidity() による一括バリデーション

novalidate 属性をフォームに付けるとブラウザのデフォルトバリデーション UI を無効にできます。独自の UI で制御したいときに使います。

checkValidity() で一括チェック
// <form novalidate> に novalidate を付けてブラウザ UI を抑制
// fields・validate 関数は「リアルタイムバリデーション」セクションで定義済み
const form = document.querySelector('form[novalidate]');

form.addEventListener('submit', (e) => {
  e.preventDefault();

  // 独自ルールで全フィールドを検証
  const results = fields.map(({ id, ruleList }) =>
    validate(document.getElementById(id), ruleList)
  );

  // HTML5 組み込みチェック(type/pattern/min 等)も併用
  if (!form.checkValidity()) {
    // HTML5 の :invalid フィールドにもカスタムエラー表示
    form.querySelectorAll(':invalid').forEach((el) => {
      el.setAttribute('aria-invalid', 'true');
      const errorEl = document.getElementById(el.getAttribute('aria-describedby'));
      if (errorEl && !errorEl.textContent) {
        errorEl.textContent = el.validationMessage; // ブラウザのメッセージをフォールバック
      }
    });
    form.querySelector(':invalid')?.focus();
    return;
  }

  if (!results.every(Boolean)) {
    form.querySelector('[aria-invalid="true"]')?.focus();
    return;
  }

  console.log('送信OK');
});

よくある質問

Qクライアント側バリデーションだけで十分ですか?
A十分ではありません。ブラウザの開発者ツールで JavaScript を無効にしたり、curl で直接リクエストを送ったりすることで、クライアント側のバリデーションは完全にスキップできます。クライアント側はあくまでUX向上のため、サーバー側でも必ず同等の検証を行ってください。
QHTML5 の required・pattern 属性と JavaScript バリデーションを両方書く必要がありますか?
A両方書くことを推奨します。HTML5 属性はブラウザが直接処理するため、JavaScript が読み込まれない環境でも機能します。JavaScript バリデーションは HTML5 では対応できないカスタムロジックや、統一されたエラー表示スタイルのために使います。
Qblur と input のどちらでリアルタイム検証すべきですか?
A一般的なベストプラクティスは「初回エラーの表示は blur(フォーカスアウト後)、エラーが出た後の解除チェックは input(入力中)」です。入力中に毎回エラーを出すと煩わしく感じるため、初回は blur まで待ちます。一度エラーが出た後は input で即座にフィードバックするとUXが向上します。
QsetCustomValidity を呼んだ後に input イベントでリセットしないとどうなりますか?
A一度 setCustomValidity でエラーメッセージを設定すると、その後ユーザーが正しい値を入力しても checkValidity() が false を返し続けます。必ず入力のたびに setCustomValidity("") でリセットしてください。
Qスクリーンリーダーにエラーを伝えるにはどうすればいいですか?
Aエラーメッセージの要素に role="alert" を付けると、テキストが変更されたとき自動的に読み上げられます。また input に aria-invalid="true" を設定し、aria-describedby でエラー要素を紐づけることで、スクリーンリーダーがどのフィールドでどんなエラーが起きているかを把握できます。

まとめ

JavaScriptのフォームバリデーションを実装するときのポイントをまとめます。

  • HTML5 属性 + JavaScript の二段構えが最も堅牢
  • ルール関数を返り値 true | エラーメッセージ文字列 で統一すると再利用しやすい
  • リアルタイム検証は「blur でエラー表示、input でエラー解除」パターンが UX に優れる
  • aria-invalidaria-describedbyrole="alert"アクセシビリティを確保する
  • 必ずサーバー側でも検証すること(クライアント検証は迂回可能)

jQuery でのバリデーション実装は【jQuery】フォームバリデーションの実装完全ガイド、classList の操作は【JavaScript】classList の使い方完全ガイドもあわせてご覧ください。