インラインバリデーション(Inline Validation)— フォームエラー表示パターン集
このコンポーネントについて
インラインバリデーションは、フォームの入力欄ごとにエラーや入力完了の状態をフィールドの直下に表示するUIパターンです。 送信後にページ上部へまとめてエラーを出す従来方式と異なり、どのフィールドに問題があるかを入力中に即座に伝えられるためユーザーの修正負担を減らせます。
このページでは2種類の実装パターンを提供します。 Pattern 1はフォーカスを外した(blur)タイミングでチェックする「問い合わせフォーム」で、入力途中に怒らせない王道の設計です。 Pattern 2は確認パスワードのリアルタイム一致チェックを加えた「新規登録フォーム」で、blur と input イベントを使い分けています。 どちらもフォーム送信時に未バリデーションのフィールドを一括確認し、全 OK なら擬似的な成功メッセージを表示します。
- blur 時バリデーション — フォーカスを外したタイミングでチェック。入力途中に怒らせない王道パターン
- エラー後リアルタイム化 — 一度エラーが出たフィールドは、再入力のたびに即座に再チェックして回復を確認できる(
data-touchedフラグで制御) - 成功フィードバック(✓) — 正しく入力できたフィールドに緑ボーダーとチェックアイコンを表示
- 送信時全チェック — 送信ボタン押下時に未バリデーションのフィールドも含めて全チェック。最初のエラーフィールドへフォーカス移動
- 確認パスワードのリアルタイム一致チェック — Pattern 2 のみ。入力するたびにパスワードと一致するか確認
- 擬似送信成功メッセージ — 全フィールド OK で送信するとフォームを非表示にして完了メッセージを表示
実装のポイント・注意点
インラインバリデーションで最も気をつけたいのが「いつバリデーションを走らせるか」です。
ページ読み込み直後から input イベントで毎回チェックすると、まだ入力が終わっていないのにエラーが出て煩わしく感じます。
そこでこのサンプルでは data-touched 属性をフラグとして使い、「一度フォーカスを外したフィールドだけリアルタイムで再チェックする」方式を採用しています。
確認パスワードは例外で、常時リアルタイムチェックが自然です。またパスワード本体を変更したとき、確認フィールドに既に値が入っていれば確認フィールドも再チェックする必要があります。 これを忘れると「一致している」と表示されたまま実際は不一致という状態になります。
<form> に novalidate 属性を必ず付けてください。付けないとブラウザネイティブのバリデーション吹き出しと自作UIが競合します。
アイコン(⚠ / ✓)とエラーメッセージはすべて textContent で書き込んでおり、innerHTML は使用していません。
なお、パスワード強度バー(弱い / 普通 / 強い / 非常に強い)や条件チェックリストの実装については パスワード入力(表示切替・強度インジケーター) をご参照ください。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
Pattern 1 — 問い合わせフォーム(blur 時バリデーション)
✓ 送信が完了しました。ありがとうございます。
Pattern 2 — 新規登録フォーム(blur+リアルタイム混在)
✓ 登録が完了しました。
サンプルソース
3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>インラインバリデーション サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<!-- Pattern 1: 問い合わせフォーム(blur 時バリデーション) -->
<div class="vld-section">
<p class="vld-section-label">Pattern 1 — 問い合わせフォーム(blur 時バリデーション)</p>
<form class="vld-form" id="contactForm" novalidate>
<div class="vld-field" id="field-name">
<label class="vld-label" for="inp-name">
お名前 <span class="vld-required">必須</span>
</label>
<div class="vld-input-wrap">
<input type="text" id="inp-name" class="vld-input"
placeholder="例:山田 太郎" autocomplete="name">
<span class="vld-icon" aria-hidden="true"></span>
</div>
<p class="vld-msg" id="msg-name"></p>
</div>
<div class="vld-field" id="field-email">
<label class="vld-label" for="inp-email">
メールアドレス <span class="vld-required">必須</span>
</label>
<div class="vld-input-wrap">
<input type="email" id="inp-email" class="vld-input"
placeholder="例:[email protected]" autocomplete="email">
<span class="vld-icon" aria-hidden="true"></span>
</div>
<p class="vld-msg" id="msg-email"></p>
</div>
<div class="vld-field" id="field-message">
<label class="vld-label" for="inp-message">
お問い合わせ内容 <span class="vld-required">必須</span>
</label>
<div class="vld-input-wrap">
<textarea id="inp-message" class="vld-input vld-textarea"
rows="4" placeholder="お問い合わせ内容を入力してください"></textarea>
</div>
<p class="vld-msg" id="msg-message"></p>
</div>
<button type="submit" class="vld-submit-btn">送信する</button>
</form>
<div class="vld-complete" id="contactComplete" hidden>
<p class="vld-complete-text">✓ 送信が完了しました。ありがとうございます。</p>
<button type="button" class="back-btn" id="contactBackBtn">戻る</button>
</div>
</div>
<!-- Pattern 2: 新規登録フォーム(blur+リアルタイム混在) -->
<div class="vld-section">
<p class="vld-section-label">Pattern 2 — 新規登録フォーム(blur+リアルタイム混在)</p>
<form class="vld-form" id="registerForm" novalidate>
<div class="vld-field" id="field-reg-email">
<label class="vld-label" for="inp-reg-email">
メールアドレス <span class="vld-required">必須</span>
</label>
<div class="vld-input-wrap">
<input type="email" id="inp-reg-email" class="vld-input"
placeholder="例:[email protected]" autocomplete="email">
<span class="vld-icon" aria-hidden="true"></span>
</div>
<p class="vld-msg" id="msg-reg-email"></p>
</div>
<div class="vld-field" id="field-password">
<label class="vld-label" for="inp-password">
パスワード <span class="vld-required">必須</span>
</label>
<div class="vld-input-wrap">
<input type="password" id="inp-password" class="vld-input"
placeholder="8文字以上" autocomplete="new-password">
<span class="vld-icon" aria-hidden="true"></span>
</div>
<p class="vld-msg" id="msg-password"></p>
</div>
<div class="vld-field" id="field-password-confirm">
<label class="vld-label" for="inp-password-confirm">
パスワード(確認) <span class="vld-required">必須</span>
</label>
<div class="vld-input-wrap">
<input type="password" id="inp-password-confirm" class="vld-input"
placeholder="もう一度入力" autocomplete="new-password">
<span class="vld-icon" aria-hidden="true"></span>
</div>
<p class="vld-msg" id="msg-password-confirm"></p>
</div>
<button type="submit" class="vld-submit-btn">登録する</button>
</form>
<div class="vld-complete" id="registerComplete" hidden>
<p class="vld-complete-text">✓ 登録が完了しました。</p>
<button type="button" class="back-btn" id="registerBackBtn">戻る</button>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
:root {
--color-accent: #2B7FE8;
--color-error: #EF4444;
--color-success: #22C55E;
--color-border: #D0D7E0;
--color-text: #2C3A4A;
}
body {
font-family: sans-serif;
background: #F4F6F9;
padding: 24px 16px;
margin: 0;
}
/* フォームカード */
.vld-section {
max-width: 480px;
margin: 0 auto 32px;
padding: 24px;
background: #fff;
border: 1px solid var(--color-border);
border-radius: 10px;
}
.vld-section-label {
font-size: 12px;
font-weight: 700;
color: var(--color-accent);
margin: 0 0 20px;
letter-spacing: 0.04em;
}
/* フィールド */
.vld-field { margin-bottom: 20px; }
.vld-label {
display: block;
margin-bottom: 6px;
font-size: 14px;
font-weight: 600;
color: var(--color-text);
}
.vld-required {
display: inline-block;
font-size: 11px;
font-weight: 700;
color: #fff;
background: var(--color-error);
padding: 1px 5px;
border-radius: 3px;
margin-left: 4px;
vertical-align: middle;
}
.vld-input-wrap { position: relative; }
.vld-input {
width: 100%;
padding: 10px 40px 10px 12px;
border: 1.5px solid var(--color-border);
border-radius: 6px;
font-size: 15px;
outline: none;
transition: border-color 0.15s;
box-sizing: border-box;
font-family: sans-serif;
background: #fff;
}
.vld-input:focus { border-color: var(--color-accent); }
/* textarea はアイコンなし */
.vld-textarea {
padding-right: 12px;
resize: vertical;
min-height: 96px;
}
/* バリデーション状態 */
.vld-field--error .vld-input { border-color: var(--color-error); }
.vld-field--success .vld-input { border-color: var(--color-success); }
/* アイコン(⚠ / ✓) */
.vld-icon {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
pointer-events: none;
line-height: 1;
}
.vld-field--error .vld-icon { color: var(--color-error); }
.vld-field--success .vld-icon { color: var(--color-success); }
/* エラーメッセージ */
.vld-msg {
margin: 4px 0 0;
font-size: 12px;
min-height: 16px; /* レイアウトシフト防止 */
line-height: 1.4;
}
.vld-field--error .vld-msg { color: var(--color-error); }
/* 送信ボタン */
.vld-submit-btn {
display: block;
width: 100%;
padding: 12px;
background: var(--color-accent);
color: #fff;
font-size: 15px;
font-weight: 600;
border: none;
border-radius: 6px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s;
margin-top: 8px;
}
.vld-submit-btn:hover { background: #1A6ED4; }
/* 完了メッセージ */
.vld-complete {
padding: 20px;
background: #F0FDF4;
border: 1.5px solid var(--color-success);
border-radius: 8px;
margin-top: 16px;
}
.vld-complete-text {
color: #15803D;
font-weight: 600;
margin: 0 0 12px;
}
/* 戻るボタン */
.back-btn {
padding: 6px 16px;
font-size: 13px;
color: #5A6A7A;
background: #fff;
border: 1.5px solid var(--color-border);
border-radius: 6px;
cursor: pointer;
font-family: sans-serif;
transition: background 0.15s, border-color 0.15s;
}
.back-btn:hover {
background: #F4F6F9;
border-color: #9AA5B4;
}
// メール形式チェック
function isValidEmail(val) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val);
}
// フィールドをエラー状態にする
function setFieldError(fieldEl, iconEl, msgEl, message) {
fieldEl.classList.add('vld-field--error');
fieldEl.classList.remove('vld-field--success');
if (iconEl) iconEl.textContent = '⚠';
msgEl.textContent = message;
}
// フィールドを成功状態にする
function setFieldSuccess(fieldEl, iconEl, msgEl) {
fieldEl.classList.add('vld-field--success');
fieldEl.classList.remove('vld-field--error');
if (iconEl) iconEl.textContent = '✓';
msgEl.textContent = '';
}
// ========================
// Pattern 1: 問い合わせフォーム
// ========================
var contactForm = document.getElementById('contactForm');
var inpName = document.getElementById('inp-name');
var inpEmail = document.getElementById('inp-email');
var inpMessage = document.getElementById('inp-message');
function validateName() {
var f = document.getElementById('field-name');
var icon = f.querySelector('.vld-icon');
var msg = document.getElementById('msg-name');
if (!inpName.value.trim()) {
setFieldError(f, icon, msg, 'お名前を入力してください');
return false;
}
setFieldSuccess(f, icon, msg);
return true;
}
function validateEmail() {
var f = document.getElementById('field-email');
var icon = f.querySelector('.vld-icon');
var msg = document.getElementById('msg-email');
var val = inpEmail.value.trim();
if (!val) {
setFieldError(f, icon, msg, 'メールアドレスを入力してください');
return false;
}
if (!isValidEmail(val)) {
setFieldError(f, icon, msg, '正しいメールアドレスを入力してください');
return false;
}
setFieldSuccess(f, icon, msg);
return true;
}
function validateMessage() {
var f = document.getElementById('field-message');
var msg = document.getElementById('msg-message');
// textarea はアイコンなし
if (!inpMessage.value.trim()) {
f.classList.add('vld-field--error');
f.classList.remove('vld-field--success');
msg.textContent = 'お問い合わせ内容を入力してください';
return false;
}
f.classList.add('vld-field--success');
f.classList.remove('vld-field--error');
msg.textContent = '';
return true;
}
// blur 時バリデーション → エラー後リアルタイム化
inpName.addEventListener('blur', function() {
this.dataset.touched = 'true';
validateName();
});
inpName.addEventListener('input', function() {
if (this.dataset.touched === 'true') validateName();
});
inpEmail.addEventListener('blur', function() {
this.dataset.touched = 'true';
validateEmail();
});
inpEmail.addEventListener('input', function() {
if (this.dataset.touched === 'true') validateEmail();
});
inpMessage.addEventListener('blur', function() {
this.dataset.touched = 'true';
validateMessage();
});
inpMessage.addEventListener('input', function() {
if (this.dataset.touched === 'true') validateMessage();
});
contactForm.addEventListener('submit', function(e) {
e.preventDefault();
var ok1 = validateName();
var ok2 = validateEmail();
var ok3 = validateMessage();
if (!ok1 || !ok2 || !ok3) {
// 最初のエラーフィールドにフォーカスを移動
var firstErr = contactForm.querySelector('.vld-field--error input, .vld-field--error textarea');
if (firstErr) firstErr.focus();
return;
}
// 全 OK → 成功メッセージを表示
contactForm.hidden = true;
document.getElementById('contactComplete').hidden = false;
});
document.getElementById('contactBackBtn').addEventListener('click', function() {
contactForm.reset();
contactForm.querySelectorAll('.vld-field').forEach(function(f) {
f.classList.remove('vld-field--error', 'vld-field--success');
var icon = f.querySelector('.vld-icon');
var msg = f.querySelector('.vld-msg');
if (icon) icon.textContent = '';
if (msg) msg.textContent = '';
});
[inpName, inpEmail, inpMessage].forEach(function(el) {
el.removeAttribute('data-touched');
});
contactForm.hidden = false;
document.getElementById('contactComplete').hidden = true;
});
// ========================
// Pattern 2: 新規登録フォーム
// ========================
var registerForm = document.getElementById('registerForm');
var inpRegEmail = document.getElementById('inp-reg-email');
var inpPassword = document.getElementById('inp-password');
var inpPasswordConfirm = document.getElementById('inp-password-confirm');
function validateRegEmail() {
var f = document.getElementById('field-reg-email');
var icon = f.querySelector('.vld-icon');
var msg = document.getElementById('msg-reg-email');
var val = inpRegEmail.value.trim();
if (!val) {
setFieldError(f, icon, msg, 'メールアドレスを入力してください');
return false;
}
if (!isValidEmail(val)) {
setFieldError(f, icon, msg, '正しいメールアドレスを入力してください');
return false;
}
setFieldSuccess(f, icon, msg);
return true;
}
function validatePassword() {
var f = document.getElementById('field-password');
var icon = f.querySelector('.vld-icon');
var msg = document.getElementById('msg-password');
var val = inpPassword.value;
if (!val) {
setFieldError(f, icon, msg, 'パスワードを入力してください');
return false;
}
if (val.length < 8) {
setFieldError(f, icon, msg, 'パスワードは8文字以上で入力してください');
return false;
}
setFieldSuccess(f, icon, msg);
return true;
}
function validatePasswordConfirm() {
var f = document.getElementById('field-password-confirm');
var icon = f.querySelector('.vld-icon');
var msg = document.getElementById('msg-password-confirm');
var val = inpPasswordConfirm.value;
if (!val) {
setFieldError(f, icon, msg, 'パスワード(確認)を入力してください');
return false;
}
if (val !== inpPassword.value) {
setFieldError(f, icon, msg, 'パスワードが一致しません');
return false;
}
setFieldSuccess(f, icon, msg);
return true;
}
// メール・パスワード: blur 後リアルタイム化
inpRegEmail.addEventListener('blur', function() {
this.dataset.touched = 'true';
validateRegEmail();
});
inpRegEmail.addEventListener('input', function() {
if (this.dataset.touched === 'true') validateRegEmail();
});
inpPassword.addEventListener('blur', function() {
this.dataset.touched = 'true';
validatePassword();
});
inpPassword.addEventListener('input', function() {
if (this.dataset.touched === 'true') validatePassword();
// パスワード変更時は確認フィールドも再チェック
if (inpPasswordConfirm.value) validatePasswordConfirm();
});
// 確認パスワード: 常時リアルタイム
inpPasswordConfirm.addEventListener('input', validatePasswordConfirm);
inpPasswordConfirm.addEventListener('blur', validatePasswordConfirm);
registerForm.addEventListener('submit', function(e) {
e.preventDefault();
var ok1 = validateRegEmail();
var ok2 = validatePassword();
var ok3 = validatePasswordConfirm();
if (!ok1 || !ok2 || !ok3) {
var firstErr = registerForm.querySelector('.vld-field--error input');
if (firstErr) firstErr.focus();
return;
}
registerForm.hidden = true;
document.getElementById('registerComplete').hidden = false;
});
document.getElementById('registerBackBtn').addEventListener('click', function() {
registerForm.reset();
registerForm.querySelectorAll('.vld-field').forEach(function(f) {
f.classList.remove('vld-field--error', 'vld-field--success');
var icon = f.querySelector('.vld-icon');
var msg = f.querySelector('.vld-msg');
if (icon) icon.textContent = '';
if (msg) msg.textContent = '';
});
[inpRegEmail, inpPassword, inpPasswordConfirm].forEach(function(el) {
el.removeAttribute('data-touched');
});
registerForm.hidden = false;
document.getElementById('registerComplete').hidden = true;
});
AI用プロンプト
各パターンのプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。ライブラリ指定や項目変更など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
Pattern 1 — 問い合わせフォーム(blur 時バリデーション)
# インラインバリデーション(問い合わせフォーム)作成依頼
## 概要
フィールドごとにエラー・成功フィードバックをインラインで表示する問い合わせフォームを実装してください。
## 要件
- フィールド:お名前(必須)、メールアドレス(必須 + メール形式)、お問い合わせ内容(必須、textarea)
- フォーカスを離れた(blur)タイミングでバリデーションを実行する
- エラー時:赤ボーダー + ⚠ アイコン + フィールド直下にエラーメッセージ表示
- 成功時:緑ボーダー + ✓ アイコン表示
- 一度エラーが出たフィールドは、再入力のたびにリアルタイムで再チェックする(data-touched フラグで制御)
- 送信ボタン押下時に未チェックのフィールドも含めて全バリデーションを実行し、最初のエラーフィールドにフォーカスを移動する
- 全フィールド OK で送信するとフォームを非表示にして「送信が完了しました。ありがとうございます。」メッセージを表示する
- 「戻る」ボタンでフォームを再表示・全フィールドをリセットする
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要
- <form> に novalidate 属性を付けてブラウザネイティブバリデーションを抑制する
## 動作詳細
各フィールドに data-touched 属性をフラグとして持たせ、blur 時に 'true' をセットする。
input イベントは dataset.touched === 'true' の場合のみバリデーションを走らせる。
アイコンとエラーメッセージは textContent で設定し、innerHTML は使用しない。
メール形式チェックは /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) で行う。
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。
Pattern 2 — 新規登録フォーム(blur+リアルタイム混在)
# インラインバリデーション(新規登録フォーム)作成依頼
## 概要
メールアドレス・パスワード・確認パスワードの3フィールドで構成される新規登録フォームを実装してください。
確認パスワードのリアルタイム一致チェックが要件です。
## 要件
- フィールド:メールアドレス(必須 + メール形式)、パスワード(必須 + 8文字以上)、パスワード(確認)(必須 + 一致チェック)
- メールアドレス・パスワードは blur 時にバリデーション。エラー後はリアルタイムで再チェック
- パスワード(確認)は1文字入力するたびにリアルタイムで一致チェックを行う
- エラー時:赤ボーダー + ⚠ アイコン + フィールド直下にエラーメッセージ
- 成功時:緑ボーダー + ✓ アイコン
- 登録ボタン押下時に全フィールドをバリデーション。全 OK 時は「登録が完了しました。」メッセージを表示しフォームを非表示にする
- 「戻る」ボタンでリセット
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:必要
- <form> に novalidate 属性を付けてブラウザネイティブバリデーションを抑制する
## 動作詳細
確認パスワードフィールドは input イベントで常時リアルタイムチェックする(touched フラグ不要)。
パスワード本体フィールドの input イベントでも、確認フィールドに値がある場合は確認フィールドを再チェックする(パスワードを変更したときに確認欄の状態が陳腐化するのを防ぐため)。
アイコン・メッセージは textContent で設定し innerHTML は使用しない。
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。