フォームバリデーションは「ユーザーが正しい形式でデータを入力しているか」をチェックする処理です。送信後のサーバーエラーを減らし、ユーザーに即座にフィードバックを与えることでUXが大幅に向上します。
この記事では、HTML5 属性だけでは対応できないカスタムバリデーションの実装を中心に、よく使う正規表現パターン集、リアルタイム検証、再利用可能な設計、スクリーンリーダー対応のアクセシビリティまで体系的に解説します。
HTML5 属性バリデーション vs JavaScript バリデーション
まず「どちらを使うべきか」を整理します。答えは両方を組み合わせることです。
| HTML5 属性 | JavaScript | |
|---|---|---|
| 設定の手軽さ | ◎ 属性1つで動く | △ コードが必要 |
| エラーメッセージのカスタマイズ | △ ブラウザ依存で見た目が変わる | ◎ 完全に制御できる |
| リアルタイム検証 | × 基本は送信時のみ | ◎ blur/input で随時検証可 |
| 複数フィールドの依存チェック | × 単一フィールドのみ | ◎ 任意のロジックが書ける |
| アクセシビリティ | ○ ブラウザが処理 | ◎ aria-invalid 等で細かく制御 |
required・type="email"・minlength 等で基本チェックを行い、カスタムメッセージや複雑なルールは JavaScript で追加する二段構えが最も堅牢です。必ずサーバー側でも検証してください。クライアント側の検証は無効化できます。エラー表示の基本設計
バリデーションの実装前に、エラーを表示する HTML・CSS の構造を統一しておきます。
<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>
.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="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 にエラーを反映する汎用関数を作ります。
/**
* @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 | 全フィールドを一括検証 |
<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向上に効果的です。
<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>
.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 から検証状態を操作できます。カスタムメッセージの設定に特に便利です。
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 で制御したいときに使います。
// <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');
});
よくある質問
setCustomValidity("") でリセットしてください。role="alert" を付けると、テキストが変更されたとき自動的に読み上げられます。また input に aria-invalid="true" を設定し、aria-describedby でエラー要素を紐づけることで、スクリーンリーダーがどのフィールドでどんなエラーが起きているかを把握できます。まとめ
JavaScriptのフォームバリデーションを実装するときのポイントをまとめます。
- HTML5 属性 + JavaScript の二段構えが最も堅牢
- ルール関数を返り値
true | エラーメッセージ文字列で統一すると再利用しやすい - リアルタイム検証は「blur でエラー表示、input でエラー解除」パターンが UX に優れる
aria-invalid・aria-describedby・role="alert"でアクセシビリティを確保する- 必ずサーバー側でも検証すること(クライアント検証は迂回可能)
jQuery でのバリデーション実装は【jQuery】フォームバリデーションの実装完全ガイド、classList の操作は【JavaScript】classList の使い方完全ガイドもあわせてご覧ください。