Convalida l'input dopo l'interazione

Funzionalità web: :user-valid e :user-invalid

Il problema

Visualizzare gli errori di convalida nel momento in cui un utente si concentra su un campo e inizia a digitare è prematuro e fonte di distrazione. Ad esempio, mentre un utente digita un indirizzo email (ad es. "user@gm") o una password con requisiti complessi, il campo non è tecnicamente valido fino al completamento. Lo stile :invalid standard genera immediatamente uno stato di errore, frustrando l'utente.

La soluzione

La pseudo-classe :user-invalid ti consente di posticipare lo stato di errore finché l'utente non ha "confermato" un valore (sfocando il campo) o non ha tentato di inviare il modulo. In questo modo, il feedback di convalida viene fornito solo dopo che l'utente ha terminato di interagire con il campo.

Strategia di implementazione

  1. Vincolo HTML: utilizza attributi HTML5 standard come type="email", pattern e required per attivare la logica di convalida integrata del browser.
  2. Feedback visivo: utilizza :user-invalid per applicare lo stile di errore solo dopo l'interazione.
  3. Rinforzo positivo: utilizza facoltativamente :user-valid per fornire un indicatore verde di "successo" una volta soddisfatti i requisiti.
  4. Ripristino regolare: non appena l'utente corregge l'input in un formato valido, :user-invalid interrompe la corrispondenza, rimuovendo immediatamente lo stato di errore.

Guida all'implementazione

Caso d'uso 1: convalida dell'email

OBBLIGATORIO: utilizza gli attributi HTML5 standard per i campi email. Il messaggio di errore è nascosto per impostazione predefinita e viene visualizzato solo quando il browser determina che l'utente ha lasciato il campo in uno stato non valido.

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

Caso d'uso 2: complessità della password

OBBLIGATORIO: definisci la regola di complessità utilizzando un pattern di lookahead regex nell'attributo pattern. L'elenco delle regole viene mostrato sopra l'input per guidare l'utente ed è evidenziato in caso di errore.

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

Fallback e supporto del browser

Fallback e supporto del browser per :user-valid e :user-invalid

CSS per il fallback

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

Fallback JavaScript

Utilizza un'utilità riutilizzabile che monitora lo stato dell'interazione utilizzando un WeakMap. In questo modo, eviti di inquinare il DOM con classi o attributi di dati "sporchi".

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

Altre considerazioni

  1. Accessibilità:
    • OBBLIGATORIO: utilizza aria-describedby per collegare l'elenco di regole all'input.
    • NON nascondere completamente gli elenchi di regole finché l'input non è valido; gli utenti devono sapere cosa digitare.
  2. Limiti degli attributi dei pattern: OBBLIGATORIO: l'attributo pattern esegue una corrispondenza completa (^...$ implicito). Assicurati che l'espressione regolare della password tenga conto dell'intera stringa.
  3. Rigidità della convalida: tieni presente che la convalida type="email" predefinita del browser è piuttosto permissiva (ad es. user@localserver potrebbe essere accettato). Se hai bisogno di una convalida più rigorosa, potresti dover utilizzare una libreria di convalida più efficace o una funzione di convalida personalizzata insieme a type="email".
  4. Gestione dello stato attivo: OBBLIGATORIO: se un utente invia il modulo con un campo non valido, il browser imposterà automaticamente lo stato attivo sul primo campo non valido. I tuoi stili :user-invalid verranno applicati immediatamente perché un tentativo di invio viene conteggiato come un'interazione.
  5. Esperienza ARIA coerente: l'elemento :user-invalid nativo non si sincronizza automaticamente con gli attributi ARIA. Aggiungi il seguente codice JavaScript per mantenere aria-invalid sincronizzato con lo stato visivo:
// 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);
});