リアルタイムバリデーション

入力のたびに正規表現で氏名・メール・電話を即時検証し、枠色とメッセージで結果を表示。全項目が有効になると送信ボタンが解放されます。

#javascript#form#validation

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<div class="sk-screen">
  <form class="rv-form" novalidate>
    <div class="sk-head">
      <span class="sk-logo">🌸 Sakura</span>
      <span class="sk-badge">OFFICIAL FAN CLUB</span>
    </div>
    <h2 class="rv-title">ファンクラブ入会フォーム</h2>
    <p class="sk-lead">「桜の咲くころ会」へようこそ。会員情報を入力してください。</p>

    <label class="rv-field" data-rule="name">
      <span class="rv-label">お名前(ニックネーム可)</span>
      <input class="rv-input" type="text" name="name" placeholder="さくら ちゃん" autocomplete="off">
      <span class="rv-msg"></span>
    </label>

    <label class="rv-field" data-rule="email">
      <span class="rv-label">メールアドレス</span>
      <input class="rv-input" type="email" name="email" placeholder="you@example.com" autocomplete="off">
      <span class="rv-msg"></span>
    </label>

    <label class="rv-field" data-rule="tel">
      <span class="rv-label">電話番号</span>
      <input class="rv-input" type="tel" name="tel" placeholder="09012345678" autocomplete="off">
      <span class="rv-msg"></span>
    </label>

    <button class="rv-btn" type="submit" disabled>入会を申し込む 🌸</button>
  </form>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background:
    radial-gradient(120% 80% at 50% -10%, #ffd1e0 0%, #ffe8f0 45%, #fbf3f6 100%);
  color: #6a4250;
}

.rv-form {
  width: min(370px, 92vw);
  padding: 22px 24px 22px;
  background: #ffffff;
  border: 1px solid #f6d8e3;
  border-radius: 18px;
  box-shadow: 0 22px 50px -24px rgba(214, 120, 160, 0.55);
}

.sk-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.sk-logo { font-size: 0.95rem; font-weight: 800; color: #e87fa5; letter-spacing: 0.04em; }
.sk-badge {
  font-size: 0.6rem; font-weight: 700; letter-spacing: 0.1em;
  color: #d96a92; padding: 3px 8px;
  background: #ffe3ee; border-radius: 99px;
}

.rv-title { margin: 0 0 4px; font-size: 1.08rem; font-weight: 800; color: #5a3344; }
.sk-lead { margin: 0 0 16px; font-size: 0.74rem; line-height: 1.5; color: #a87f90; }

.rv-field { display: block; margin-bottom: 12px; }
.rv-label { display: block; margin-bottom: 5px; font-size: 0.74rem; font-weight: 600; color: #b07f92; }

.rv-input {
  width: 100%;
  padding: 10px 13px;
  font-size: 0.92rem;
  color: #5a3344;
  background: #fff7fb;
  border: 1.5px solid #f1cdda;
  border-radius: 11px;
  outline: none;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.rv-input::placeholder { color: #d4a8b8; }
.rv-input:focus { border-color: #ec86ab; box-shadow: 0 0 0 3px rgba(236, 134, 171, 0.22); }

/* 状態クラスで枠色を切り替え(技法の主役) */
.rv-field.is-valid .rv-input { border-color: #5ec98c; background: #f4fdf7; }
.rv-field.is-invalid .rv-input { border-color: #f3819a; background: #fff5f7; }

.rv-msg {
  display: block; min-height: 15px; margin-top: 4px;
  font-size: 0.72rem; line-height: 1.3;
  transition: opacity 0.18s ease;
}
.rv-field.is-valid .rv-msg { color: #3fae71; }
.rv-field.is-invalid .rv-msg { color: #e35d7c; }

.rv-btn {
  width: 100%; margin-top: 6px; padding: 12px;
  font-size: 0.94rem; font-weight: 700; letter-spacing: 0.04em;
  color: #fff;
  background: linear-gradient(135deg, #ff9cc0, #e8709a);
  border: none; border-radius: 12px; cursor: pointer;
  box-shadow: 0 12px 22px -10px rgba(232, 112, 154, 0.8);
  transition: opacity 0.18s ease, transform 0.15s ease;
}
.rv-btn:disabled { opacity: 0.45; cursor: not-allowed; box-shadow: none; }
.rv-btn:not(:disabled):hover { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .rv-input, .rv-msg, .rv-btn { transition: none; }
}
JavaScript
// 各ルールの検証関数。trueなら成功、文字列なら失敗理由
const rules = {
  name: (v) => v.trim().length >= 2 || "2文字以上で入力してください",
  email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || "メール形式が正しくありません",
  tel: (v) => /^0\d{9,10}$/.test(v.replace(/[-\s]/g, "")) || "電話番号が正しくありません",
};
const okText = { name: "OK", email: "有効なメールです", tel: "OK" };

const form = document.querySelector(".rv-form");
const fields = form ? [...form.querySelectorAll(".rv-field")] : [];
const btn = form ? form.querySelector(".rv-btn") : null;

// 1フィールドを検証して状態クラス/メッセージを更新
function validateField(field) {
  const key = field.dataset.rule;
  const input = field.querySelector(".rv-input");
  const msg = field.querySelector(".rv-msg");
  if (!input || !msg || !rules[key]) return false;

  const empty = input.value.trim() === "";
  if (empty) {
    field.classList.remove("is-valid", "is-invalid");
    msg.textContent = "";
    return false;
  }
  const result = rules[key](input.value);
  const valid = result === true;
  field.classList.toggle("is-valid", valid);
  field.classList.toggle("is-invalid", !valid);
  msg.textContent = valid ? okText[key] : result;
  return valid;
}

// 全フィールドが有効なら入会ボタン解放
function refreshButton() {
  if (!btn) return;
  const allValid = fields.every((f) => f.classList.contains("is-valid"));
  btn.disabled = !allValid;
}

fields.forEach((field) => {
  const input = field.querySelector(".rv-input");
  if (!input) return;
  // 入力のたびにリアルタイム検証
  input.addEventListener("input", () => { validateField(field); refreshButton(); });
  input.addEventListener("blur", () => { validateField(field); refreshButton(); });
});

if (form) {
  form.addEventListener("submit", (e) => {
    e.preventDefault(); // 実送信しない
    fields.forEach(validateField);
    refreshButton();
    if (!btn || btn.disabled) return;
    const keep = btn.textContent;
    btn.textContent = "入会を受け付けました ✓";
    setTimeout(() => (btn.textContent = keep), 1800);
  });
}

コード

HTML
<div class="stage">
  <form class="rv-form" novalidate>
    <h2 class="rv-title">お問い合わせ</h2>

    <label class="rv-field" data-rule="name">
      <span class="rv-label">お名前</span>
      <input class="rv-input" type="text" name="name" placeholder="山田 太郎" autocomplete="off">
      <span class="rv-msg"></span>
    </label>

    <label class="rv-field" data-rule="email">
      <span class="rv-label">メール</span>
      <input class="rv-input" type="email" name="email" placeholder="you@example.com" autocomplete="off">
      <span class="rv-msg"></span>
    </label>

    <label class="rv-field" data-rule="tel">
      <span class="rv-label">電話番号</span>
      <input class="rv-input" type="tel" name="tel" placeholder="09012345678" autocomplete="off">
      <span class="rv-msg"></span>
    </label>

    <button class="rv-btn" type="submit" disabled>送信</button>
  </form>
</div>
CSS
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: radial-gradient(circle at 30% 20%, #2b3a55, #161d2e 70%);
  color: #e5e7eb;
}

.stage { width: 100%; padding: 22px; display: grid; place-items: center; }

.rv-form {
  width: min(360px, 92vw);
  padding: 26px 24px 22px;
  background: #1e2638;
  border: 1px solid #2c3650;
  border-radius: 16px;
  box-shadow: 0 22px 50px -22px rgba(0, 0, 0, 0.7);
}

.rv-title { margin: 0 0 18px; font-size: 1.1rem; letter-spacing: 0.04em; }

.rv-field { display: block; margin-bottom: 14px; }
.rv-label { display: block; margin-bottom: 6px; font-size: 0.78rem; color: #9aa6c0; letter-spacing: 0.04em; }

.rv-input {
  width: 100%;
  padding: 11px 13px;
  font-size: 0.95rem;
  color: #f1f5f9;
  background: #141a28;
  border: 1.5px solid #38425e;
  border-radius: 10px;
  outline: none;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.rv-input:focus { border-color: #5b8def; box-shadow: 0 0 0 3px rgba(91, 141, 239, 0.22); }

/* 状態クラスで枠色を切り替え */
.rv-field.is-valid .rv-input { border-color: #34d399; }
.rv-field.is-invalid .rv-input { border-color: #f87171; }

.rv-msg {
  display: block;
  min-height: 16px;
  margin-top: 5px;
  font-size: 0.74rem;
  line-height: 1.3;
  transition: opacity 0.18s ease;
}
.rv-field.is-valid .rv-msg { color: #34d399; }
.rv-field.is-invalid .rv-msg { color: #f87171; }

.rv-btn {
  width: 100%;
  margin-top: 6px;
  padding: 12px;
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #fff;
  background: linear-gradient(135deg, #5b8def, #6366f1);
  border: none;
  border-radius: 10px;
  cursor: pointer;
  transition: opacity 0.18s ease, transform 0.15s ease;
}
.rv-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.rv-btn:not(:disabled):hover { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .rv-input, .rv-msg, .rv-btn { transition: none; }
}
JavaScript
// 各ルールの検証関数。trueなら成功メッセージ、文字列なら失敗理由
const rules = {
  name: (v) => v.trim().length >= 2 || "2文字以上で入力してください",
  email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || "メール形式が正しくありません",
  tel: (v) => /^0\d{9,10}$/.test(v.replace(/[-\s]/g, "")) || "電話番号が正しくありません",
};
const okText = { name: "OK", email: "有効なメールです", tel: "OK" };

const form = document.querySelector(".rv-form");
const fields = form ? [...form.querySelectorAll(".rv-field")] : [];
const btn = form ? form.querySelector(".rv-btn") : null;

// 1フィールドを検証して状態クラス/メッセージを更新
function validateField(field) {
  const key = field.dataset.rule;
  const input = field.querySelector(".rv-input");
  const msg = field.querySelector(".rv-msg");
  if (!input || !msg || !rules[key]) return false;

  const empty = input.value.trim() === "";
  if (empty) {
    field.classList.remove("is-valid", "is-invalid");
    msg.textContent = "";
    return false;
  }
  const result = rules[key](input.value);
  const valid = result === true;
  field.classList.toggle("is-valid", valid);
  field.classList.toggle("is-invalid", !valid);
  msg.textContent = valid ? okText[key] : result;
  return valid;
}

// 全フィールドが有効ならボタン解放
function refreshButton() {
  if (!btn) return;
  const allValid = fields.every((f) => f.classList.contains("is-valid"));
  btn.disabled = !allValid;
}

fields.forEach((field) => {
  const input = field.querySelector(".rv-input");
  if (!input) return;
  // 入力のたびにリアルタイム検証
  input.addEventListener("input", () => { validateField(field); refreshButton(); });
  input.addEventListener("blur", () => { validateField(field); refreshButton(); });
});

if (form) {
  form.addEventListener("submit", (e) => {
    e.preventDefault();
    fields.forEach(validateField);
    refreshButton();
    if (!btn || btn.disabled) return;
    btn.textContent = "送信しました ✓";
    setTimeout(() => (btn.textContent = "送信"), 1600);
  });
}

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「リアルタイムバリデーション」の効果を追加してください。

# 追加してほしい効果
リアルタイムバリデーション(フォーム & 入力)
入力のたびに正規表現で氏名・メール・電話を即時検証し、枠色とメッセージで結果を表示。全項目が有効になると送信ボタンが解放されます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<div class="stage">
  <form class="rv-form" novalidate>
    <h2 class="rv-title">お問い合わせ</h2>

    <label class="rv-field" data-rule="name">
      <span class="rv-label">お名前</span>
      <input class="rv-input" type="text" name="name" placeholder="山田 太郎" autocomplete="off">
      <span class="rv-msg"></span>
    </label>

    <label class="rv-field" data-rule="email">
      <span class="rv-label">メール</span>
      <input class="rv-input" type="email" name="email" placeholder="you@example.com" autocomplete="off">
      <span class="rv-msg"></span>
    </label>

    <label class="rv-field" data-rule="tel">
      <span class="rv-label">電話番号</span>
      <input class="rv-input" type="tel" name="tel" placeholder="09012345678" autocomplete="off">
      <span class="rv-msg"></span>
    </label>

    <button class="rv-btn" type="submit" disabled>送信</button>
  </form>
</div>

【CSS】
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", "Hiragino Kaku Gothic ProN", system-ui, sans-serif;
  background: radial-gradient(circle at 30% 20%, #2b3a55, #161d2e 70%);
  color: #e5e7eb;
}

.stage { width: 100%; padding: 22px; display: grid; place-items: center; }

.rv-form {
  width: min(360px, 92vw);
  padding: 26px 24px 22px;
  background: #1e2638;
  border: 1px solid #2c3650;
  border-radius: 16px;
  box-shadow: 0 22px 50px -22px rgba(0, 0, 0, 0.7);
}

.rv-title { margin: 0 0 18px; font-size: 1.1rem; letter-spacing: 0.04em; }

.rv-field { display: block; margin-bottom: 14px; }
.rv-label { display: block; margin-bottom: 6px; font-size: 0.78rem; color: #9aa6c0; letter-spacing: 0.04em; }

.rv-input {
  width: 100%;
  padding: 11px 13px;
  font-size: 0.95rem;
  color: #f1f5f9;
  background: #141a28;
  border: 1.5px solid #38425e;
  border-radius: 10px;
  outline: none;
  transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.rv-input:focus { border-color: #5b8def; box-shadow: 0 0 0 3px rgba(91, 141, 239, 0.22); }

/* 状態クラスで枠色を切り替え */
.rv-field.is-valid .rv-input { border-color: #34d399; }
.rv-field.is-invalid .rv-input { border-color: #f87171; }

.rv-msg {
  display: block;
  min-height: 16px;
  margin-top: 5px;
  font-size: 0.74rem;
  line-height: 1.3;
  transition: opacity 0.18s ease;
}
.rv-field.is-valid .rv-msg { color: #34d399; }
.rv-field.is-invalid .rv-msg { color: #f87171; }

.rv-btn {
  width: 100%;
  margin-top: 6px;
  padding: 12px;
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #fff;
  background: linear-gradient(135deg, #5b8def, #6366f1);
  border: none;
  border-radius: 10px;
  cursor: pointer;
  transition: opacity 0.18s ease, transform 0.15s ease;
}
.rv-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.rv-btn:not(:disabled):hover { transform: translateY(-2px); }

@media (prefers-reduced-motion: reduce) {
  .rv-input, .rv-msg, .rv-btn { transition: none; }
}

【JavaScript】
// 各ルールの検証関数。trueなら成功メッセージ、文字列なら失敗理由
const rules = {
  name: (v) => v.trim().length >= 2 || "2文字以上で入力してください",
  email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) || "メール形式が正しくありません",
  tel: (v) => /^0\d{9,10}$/.test(v.replace(/[-\s]/g, "")) || "電話番号が正しくありません",
};
const okText = { name: "OK", email: "有効なメールです", tel: "OK" };

const form = document.querySelector(".rv-form");
const fields = form ? [...form.querySelectorAll(".rv-field")] : [];
const btn = form ? form.querySelector(".rv-btn") : null;

// 1フィールドを検証して状態クラス/メッセージを更新
function validateField(field) {
  const key = field.dataset.rule;
  const input = field.querySelector(".rv-input");
  const msg = field.querySelector(".rv-msg");
  if (!input || !msg || !rules[key]) return false;

  const empty = input.value.trim() === "";
  if (empty) {
    field.classList.remove("is-valid", "is-invalid");
    msg.textContent = "";
    return false;
  }
  const result = rules[key](input.value);
  const valid = result === true;
  field.classList.toggle("is-valid", valid);
  field.classList.toggle("is-invalid", !valid);
  msg.textContent = valid ? okText[key] : result;
  return valid;
}

// 全フィールドが有効ならボタン解放
function refreshButton() {
  if (!btn) return;
  const allValid = fields.every((f) => f.classList.contains("is-valid"));
  btn.disabled = !allValid;
}

fields.forEach((field) => {
  const input = field.querySelector(".rv-input");
  if (!input) return;
  // 入力のたびにリアルタイム検証
  input.addEventListener("input", () => { validateField(field); refreshButton(); });
  input.addEventListener("blur", () => { validateField(field); refreshButton(); });
});

if (form) {
  form.addEventListener("submit", (e) => {
    e.preventDefault();
    fields.forEach(validateField);
    refreshButton();
    if (!btn || btn.disabled) return;
    btn.textContent = "送信しました ✓";
    setTimeout(() => (btn.textContent = "送信"), 1600);
  });
}

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。