Valider la saisie après l'interaction

Fonctionnalités Web : :user-valid et :user-invalid

Le problème

Afficher des erreurs de validation dès qu'un utilisateur sélectionne un champ et commence à taper est prématuré et distrayant. Par exemple, lorsqu'un utilisateur saisit une adresse e-mail (par exemple, "user@gm") ou un mot de passe avec des exigences complexes, le champ est techniquement non valide jusqu'à ce qu'il soit complété. Le style :invalid standard entraîne l'affichage immédiat d'un état d'erreur, ce qui frustre l'utilisateur.

La solution

La pseudo-classe :user-invalid vous permet de différer l'état d'erreur jusqu'à ce que l'utilisateur ait "validé" une valeur (en floutant le champ) ou tenté d'envoyer le formulaire. Cela garantit que les commentaires de validation ne sont fournis qu'une fois que l'utilisateur a fini d'interagir avec le champ.

Stratégie d'implémentation

  1. Contrainte HTML : utilisez des attributs HTML5 standards tels que type="email", pattern et required pour déclencher la logique de validation intégrée du navigateur.
  2. Commentaires visuels : utilisez :user-invalid pour appliquer un style d'erreur uniquement après l'interaction.
  3. Renforcement positif : vous pouvez utiliser :user-valid pour afficher un indicateur de réussite vert une fois que les conditions sont remplies.
  4. Récupération fluide : dès que l'utilisateur corrige l'entrée pour qu'elle soit au bon format, :user-invalid cesse de correspondre, ce qui supprime immédiatement l'état d'erreur.

Guide d'implémentation

Cas d'utilisation 1 : Validation des adresses e-mail

OBLIGATOIRE : Utilisez des attributs HTML5 standards pour les champs d'adresse e-mail. Le message d'erreur est masqué par défaut et ne s'affiche que lorsque le navigateur détermine que l'utilisateur a quitté le champ dans un état non valide.

<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;
}

Cas d'utilisation 2 : Complexité des mots de passe

OBLIGATOIRE : Définissez la règle de complexité à l'aide d'un modèle d'assertion simple (lookahead) d'expression régulière dans l'attribut pattern. La liste des règles s'affiche au-dessus du champ de saisie pour guider l'utilisateur. Elle est mise en surbrillance en cas d'erreur.

<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;
}

Création de remplacement et compatibilité avec les navigateurs

Solutions de secours et compatibilité des navigateurs pour :user-valid et :user-invalid

CSS pour la création de remplacement

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 de remplacement

Utilisez un utilitaire réutilisable qui suit l'état d'interaction à l'aide d'un WeakMap. Cela évite de polluer le DOM avec des classes ou des attributs de données "sales".

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);

Autres points à prendre en compte

  1. Accessibilité :
    • OBLIGATOIRE : Utilisez aria-describedby pour associer la liste de règles à l'entrée.
    • À NE PAS FAIRE : Masquer complètement les listes de règles tant que la saisie n'est pas valide. Les utilisateurs doivent savoir quoi saisir.
  2. Limites de l'attribut de modèle : OBLIGATOIRE : l'attribut pattern effectue une correspondance complète (^...$ implicite). Assurez-vous que l'expression régulière de votre mot de passe tient compte de l'intégralité de la chaîne.
  3. Rigueur de la validation : notez que la validation type="email" par défaut du navigateur est assez permissive (par exemple, user@localserver peut être accepté). Si vous avez besoin d'une validation plus stricte, vous devrez peut-être utiliser une bibliothèque de validation plus robuste ou une fonction de validation personnalisée avec type="email".
  4. Gestion du curseur : OBLIGATOIRE : si un utilisateur envoie le formulaire avec un champ non valide, le navigateur place automatiquement le curseur sur le premier champ non valide. Vos styles :user-invalid s'appliqueront immédiatement, car une tentative d'envoi est considérée comme une interaction.
  5. Expérience ARIA cohérente : l'élément :user-invalid natif ne se synchronise pas automatiquement avec les attributs ARIA. Ajoutez le code JavaScript suivant pour que aria-invalid reste synchronisé avec l'état visuel :
// 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);
});