こんにちは、かつコーチです。
前回は、Intersection Observerを使ったスクロールアニメーションを解説しました。
今回のテーマは、お問い合わせフォームなどで見かける「入力しながらリアルタイムにエラーが表示される」あの仕組みです。
「送信ボタンを押した瞬間に大量のエラーが表示されて、ユーザーが萎える」というフォーム、見たことがある人も多いと思います。
今日は、入力中に少しずつフィードバックを返す、使いやすいバリデーションの実装方法を解説します。
リアルタイムバリデーションとは?
送信時チェックとの違い
バリデーションとは、入力された値が正しい形式・条件を満たしているかをチェックする処理のことです。
多くの初学者が最初に書くのは「送信ボタンが押されたタイミングでまとめてチェックする」パターンです。
これに対してリアルタイムバリデーションは、ユーザーが入力している最中、または入力欄から離れたタイミングでチェックし、その場でエラーを表示する方式です。
送信時に一気にエラーが出るより、その場で少しずつ気づけるほうがユーザーのストレスは小さくなります。
どのタイミングでチェックするのが適切か
チェックのタイミングとして使われる主なイベントは次の3つです。
| イベント | 発火タイミング | 向いている用途 |
|---|---|---|
input | 1文字入力するたびに発火 | 文字数カウント、パスワード強度表示 |
blur | 入力欄からフォーカスが外れたとき | 形式チェック(メール・電話番号など) |
submit | フォーム送信時 | 最終確認、未入力チェック |
inputイベントだけで全部チェックすると、「まだメールアドレスを打ち始めた1文字目」の時点でエラーが出てしまい、かえって邪魔になります。
そのため、「形式チェックはblur(入力欄から離れたとき)」「一度エラーが出た後の再チェックはinput(即座に消したい)」という組み合わせがよく使われます。
基本の実装
手順1:HTMLでフォームとエラー表示欄を用意する
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>リアルタイムバリデーション</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<form id="contact-form" novalidate>
<div class="field">
<label for="name">お名前</label>
<input type="text" id="name" name="name">
<p class="error-message" id="name-error"></p>
</div>
<div class="field">
<label for="email">メールアドレス</label>
<input type="text" id="email" name="email">
<p class="error-message" id="email-error"></p>
</div>
<button type="submit">送信する</button>
</form>
<script src="script.js"></script>
</body>
</html>
novalidate属性を付けているのは、ブラウザ標準のバリデーション表示と自作のバリデーション表示が二重に出るのを防ぐためです。
手順2:CSSでエラー状態を分かりやすくする
.field {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: bold;
}
input {
width: 100%;
max-width: 320px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
/* エラーがある入力欄の枠を赤くする */
input.is-invalid {
border-color: #e53935;
background: #fff5f5;
}
.error-message {
min-height: 20px;
margin: 4px 0 0;
font-size: 13px;
color: #e53935;
}
手順3:JavaScriptでチェック関数を作る
const form = document.getElementById('contact-form');
const nameInput = document.getElementById('name');
const emailInput = document.getElementById('email');
// 入力欄ごとのチェックルールをまとめておく
const rules = {
name: (value) => {
if (value.trim() === '') return 'お名前を入力してください';
return '';
},
email: (value) => {
if (value.trim() === '') return 'メールアドレスを入力してください';
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!pattern.test(value)) return 'メールアドレスの形式が正しくありません';
return '';
},
};
// 1つの入力欄をチェックして、エラー表示を更新する
function validateField(input) {
const message = rules[input.name](input.value);
const errorEl = document.getElementById(`${input.name}-error`);
if (message) {
input.classList.add('is-invalid');
errorEl.textContent = message;
} else {
input.classList.remove('is-invalid');
errorEl.textContent = '';
}
return message === '';
}
チェックルールをrulesオブジェクトにまとめておくことで、「どの項目に」「どんな条件があるか」が一目で分かるようになり、項目が増えても管理しやすくなります。
手順4:イベントを登録する
[nameInput, emailInput].forEach((input) => {
// 入力欄から離れたタイミングで最初のチェックをする
input.addEventListener('blur', () => {
validateField(input);
});
// 一度エラーが出た項目だけ、入力のたびに再チェックする
input.addEventListener('input', () => {
if (input.classList.contains('is-invalid')) {
validateField(input);
}
});
});
form.addEventListener('submit', (e) => {
e.preventDefault();
const isNameValid = validateField(nameInput);
const isEmailValid = validateField(emailInput);
if (isNameValid && isEmailValid) {
alert('送信しました(実際にはここでAPIにデータを送信します)');
form.reset();
}
});
blurで最初のチェックをし、一度エラーになった項目だけinputでリアルタイムに再チェックする、という組み合わせにすることで、「打っている途中は静かに、エラーが出た後はすぐ消える」という自然な挙動になります。
つまずきポイント:エラーが消えるタイミングがずれる
「直しているのにエラーが消えない」問題
私が最初にリアルタイムバリデーションを実装したとき、blurイベントだけでチェックを実装していました。
❌ Before:blurでしかチェックしない
[nameInput, emailInput].forEach((input) => {
input.addEventListener('blur', () => {
validateField(input);
});
});
このコードだと、一度エラーが出た後、ユーザーが正しい値に修正しても、次の入力欄に移動する(=再びblurする)までエラー表示が消えません。
実際にテストユーザーとして自分で入力してみたところ、「直したのにエラーが消えないから、直っていないのかと思った」という違和感がありました。
これは地味なようで、フォーム離脱の原因になりかねない問題です。
✅ After:エラー状態の項目はinputで即座に再チェックする
[nameInput, emailInput].forEach((input) => {
input.addEventListener('blur', () => {
validateField(input);
});
input.addEventListener('input', () => {
// is-invalidが付いている(=一度エラーになった)項目だけ即座に反応させる
if (input.classList.contains('is-invalid')) {
validateField(input);
}
});
});
「一度もエラーになっていない項目はinputで反応させない(静かなまま)」「一度エラーになった項目はinputですぐに反応させる(即座に消える)」という条件分岐を入れることで、ユーザーが違和感なく修正できるようになります。
このように「エラーを出すタイミング」と「エラーを消すタイミング」は非対称に設計するのが、使いやすいバリデーションのコツです。
応用:文字数カウントとパスワード強度の可視化
リアルタイムで文字数を表示する
inputイベントを使えば、入力中に文字数をカウントして表示するUIも簡単に作れます。
<div class="field">
<label for="message">お問い合わせ内容(100文字以内)</label>
<textarea id="message" maxlength="100"></textarea>
<p class="char-count"><span id="char-count">0</span> / 100文字</p>
</div>
const messageInput = document.getElementById('message');
const charCount = document.getElementById('char-count');
messageInput.addEventListener('input', () => {
charCount.textContent = messageInput.value.length;
});
パスワードの強度を段階的に表示する
同じ考え方で、パスワードの強度チェックも実装できます。
const passwordInput = document.getElementById('password');
const strengthEl = document.getElementById('password-strength');
function checkStrength(value) {
let score = 0;
if (value.length >= 8) score += 1;
if (/[A-Z]/.test(value)) score += 1;
if (/[0-9]/.test(value)) score += 1;
if (/[!-/:-@[-`{-~]/.test(value)) score += 1;
if (score <= 1) return { label: '弱い', className: 'weak' };
if (score <= 3) return { label: '普通', className: 'medium' };
return { label: '強い', className: 'strong' };
}
passwordInput.addEventListener('input', () => {
const result = checkStrength(passwordInput.value);
strengthEl.textContent = `強度:${result.label}`;
strengthEl.className = `password-strength ${result.className}`;
});
条件を満たすたびにスコアを加算し、スコアに応じてラベルを切り替えるだけのシンプルな仕組みですが、ユーザーには「見ながら入力できる」安心感を与えられます。
まとめ
この記事のポイント
- リアルタイムバリデーションは、送信時にまとめてチェックするより早い段階でユーザーに気づきを与えられる
- 形式チェックは
blur、一度エラーになった項目の再チェックはinput、という組み合わせが基本形になる - 「エラーが消えるタイミング」が遅いと、修正してもエラーが残って見えるので注意する
- チェックルールをオブジェクトにまとめておくと、項目が増えても管理しやすい
- 同じ
inputイベントの仕組みで、文字数カウントやパスワード強度表示にも応用できる
次に読むべき記事
次回は、スムーススクロールの実装を解説します。
→ 次の記事:スムーススクロールを実装する
