インタラクション後に入力を検証する

問題

ユーザーがフィールドにフォーカスして入力を開始した瞬間に検証エラーを表示するのは、時期尚早であり、ユーザーの注意をそらすことになります。たとえば、ユーザーがメールアドレス(「user@gm」など)や複雑な要件のパスワードを入力している場合、入力が完了するまで、そのフィールドは技術的に無効です。標準の :invalid スタイルでは、エラー状態がすぐに表示され、ユーザーをいらだたせることになります。

ソリューション

:user-invalid 疑似クラスを使用すると、ユーザーが(フィールドをぼかすことで)値を「コミット」するか、フォームを送信しようとするまで、エラー状態を遅らせることができます。これにより、ユーザーがフィールドの操作を完了した後にのみ、検証フィードバックが提供されます。

実装戦略

  1. HTML 制約: type="email"、pattern、required などの標準の HTML5 属性を使用して、ブラウザの組み込み検証ロジックをトリガーします。
  2. 視覚的なフィードバック: :user-invalid を使用して、操作後にのみエラー スタイルを適用します。
  3. ポジティブな強化: 必要に応じて :user-valid を使用し、要件が満たされたら緑色の「成功」インジケーターを表示します。
  4. スムーズな復帰: ユーザーが有効な形式に入力を修正すると、:user-invalid は一致を停止し、エラー状態を直ちに解除します。

実装ガイド

ユースケース 1: メール検証

必須: メールアドレスのフィールドには、標準の HTML5 属性を使用します。エラー メッセージはデフォルトでは非表示になっており、ユーザーが無効な状態でフィールドを離れたとブラウザが判断した場合にのみ表示されます。

<form>
  <div class="field">
    <label for="email">Email Address</label>
    <!-- MANDATORY: Place format hints above the input so autocomplete popovers don't cover them during editing -->
    <span id="email-hint" class="hint">Format: you@example.com</span>
    <!-- DO: Use standard HTML validation attributes like type="email" and required -->
    <input
      type="email"
      id="email"
      name="email"
      required
      autocomplete="email"
      aria-describedby="email-hint"
      aria-errormessage="email-error"
    >
    <div id="email-error" class="error-msg">
      <span aria-hidden="true">❌</span> Please enter a valid email address.
    </div>
  </div>
</form>
.hint {
  display: block;
  color: #5f6368;
  font-size: 0.85rem;
  margin-bottom: 0.25rem;
}

.error-msg {
  display: none;
  color: #d93025;
  font-size: 0.875rem;
  margin-top: 0.25rem;
}

/*
  DO: Only show error styles after user interaction.
  Use multiple indicators (border/background shift + icon/text) to avoid color-only states.
*/
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Reveal the error message using the adjacent sibling selector */
input:user-invalid + .error-msg {
  display: block;
}

/* DO: Provide a clear success indication on :user-valid */
input:user-valid {
  border-color: #188038;
}

ユースケース 2: パスワードの複雑さ

必須: pattern 属性で正規表現の先読みパターンを使用して、複雑さのルールを定義します。ルールのリストは、ユーザーをガイドするために入力欄の上に表示され、エラーがある場合はハイライト表示されます。

<form>
  <div class="field">
    <label for="password">New Password</label>
    <!-- MANDATORY: Place hints and rules above the input so mobile keyboards do not obscure them -->
    <ul id="password-rules" class="rules-list">
      <li>At least 8 characters</li>
      <li>One uppercase letter</li>
      <li>One number</li>
      <li>One special character</li>
    </ul>
    <!-- DO: Use pattern and minlength for complex password validation
         DO: Match all constraints with lookaheads via pattern attribute
     -->
    <input
      type="password"
      id="password"
      autocomplete="new-password"
      required
      pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_]).{8,}"
      minlength="8"
      aria-describedby="password-rules"
    >
  </div>
</form>
/* DO: State the default styling as neutral */
.rules-list { 
  color: #5f6368; 
  margin-bottom: 0.5rem;
}

/* DO: Show invalid state (After interaction): Error */
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* DO: Highlight rules list when error is shown using the modern :has() selector */
.field:has(input:user-invalid) .rules-list {
  color: #d93025;
  font-weight: 600;
}

/* DO: Add success indications for :user-valid state */
input:user-valid {
  border-color: #188038;
}

/* DO: Hide rules once satisfied */
.field:has(input:user-valid) .rules-list {
  display: none;
}

フォールバックとブラウザのサポート

:user-valid と :user-invalid のフォールバックとブラウザ サポート

代替広告の CSS

input:user-invalid,
input.user-invalid-fallback {
  border-color: #d93025;
  background-color: #fce8e6;
}

input:user-invalid + .error-msg,
input.user-invalid-fallback + .error-msg {
  display: block;
}

JavaScript フォールバック

WeakMap を使用してインタラクションの状態を追跡する、再利用可能なユーティリティを使用します。これにより、「ダーティ」なクラスやデータ属性で DOM が汚染されるのを防ぐことができます。

const UserInvalidFallback = (() => {
  const dirtyState = new WeakMap();

  const updateState = (input) => {
    const isValid = input.checkValidity();

    // Update both visual and ARIA state
    input.classList.toggle('user-invalid-fallback', !isValid);
    input.classList.toggle('user-valid-fallback', isValid);

    if (!isValid) {
      input.setAttribute('aria-invalid', 'true');
    } else {
      input.removeAttribute('aria-invalid');
    }
  };

  const handleEvent = (event) => {
    const input = event.target;

    if (event.type === 'reset') {
      const controls = input.elements || [];
      for (const control of controls) {
        dirtyState.delete(control);
        control.classList.remove('user-invalid-fallback');
        control.classList.remove('user-valid-fallback');
        control.removeAttribute('aria-invalid');
      }
      return;
    }

    if (!input.checkValidity) return;

    if (event.type === 'input' || event.type === 'change') {
      const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
      state.hasInteracted = true;
      dirtyState.set(input, state);
      if (state.hasBlurred) {
        updateState(input);
      }
    } else if (event.type === 'blur') {
      const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
      state.hasBlurred = true;
      dirtyState.set(input, state);
      if (state.hasInteracted) {
        updateState(input);
      }
    }
  };

  const init = (root = document) => {
    if (CSS.supports('selector(:user-invalid)')) return;

    root.addEventListener('blur', handleEvent, true); // Capture phase
    root.addEventListener('input', handleEvent);
    root.addEventListener('change', handleEvent);
    root.addEventListener('reset', handleEvent, true); // Capture resets
  };

  return { init };
})();

// Initialize for a specific form
const form = document.querySelector('#demo-form');
UserInvalidFallback.init(form);

その他の注意事項

  1. ユーザー補助:
    • 必須: aria-describedby を使用して、ルールリストを入力にリンクします。
    • 禁止事項: 入力が有効になるまでルールリストを完全に非表示にすること。ユーザーは、何を入力すべきかを知る必要があります。
  2. パターン属性の制限: 必須: pattern 属性は完全一致(暗黙の ^...$)を実行します。パスワードの正規表現が文字列全体を考慮していることを確認してください。
  3. 検証の厳格さ: ブラウザのデフォルトの type="email" 検証はかなり緩い(例: user@localserver がパスする可能性がある)ことに注意してください。より厳格な検証が必要な場合は、より堅牢な検証ライブラリまたはカスタム検証関数を type="email" と組み合わせて使用する必要があります。
  4. フォーカス管理: 必須: ユーザーが無効なフィールドを含むフォームを送信すると、ブラウザは自動的に最初の無効なフィールドにフォーカスします。送信の試行はインタラクションとしてカウントされるため、:user-invalid スタイルはすぐに適用されます。
  5. 一貫した ARIA エクスペリエンス: ネイティブの :user-invalid は ARIA 属性と自動的に同期されません。aria-invalid を視覚的な状態と同期させるために、次の JavaScript を追加します。
// Sync aria-invalid with the CSS :user-invalid state
const syncAria = (el) => {
  el.setAttribute?.('aria-invalid', el.matches(':user-invalid') ? 'true' : 'false');
};

// Update on blur (to show error) and input (to clear it)
document.addEventListener('blur', (e) => syncAria(e.target), true);
document.addEventListener('input', (e) => {
  if (e.target.hasAttribute('aria-invalid')) syncAria(e.target);
});