Invoer valideren na interactie

Het probleem

Het is voorbarig en afleidend om validatiefouten te tonen op het moment dat een gebruiker een veld selecteert en begint te typen. Als een gebruiker bijvoorbeeld een e-mailadres (zoals 'gebruiker@gm') of een wachtwoord met complexe vereisten typt, is het veld technisch ongeldig totdat het is ingevuld. Standaard :invalid-stijlen leiden ertoe dat er onmiddellijk een foutstatus verschijnt, wat frustrerend is voor de gebruiker.

De oplossing

Met de pseudoklasse :user-invalid kun je de foutstatus uitstellen totdat de gebruiker een waarde heeft vastgelegd (door het veld te vervagen) of het formulier heeft proberen in te dienen. Zo zorg je dat er pas validatiefeedback wordt gegeven nadat de gebruiker klaar is met de interactie met het veld.

Implementatiestrategie

  1. HTML-beperking: Gebruik WEL standaard HTML5-kenmerken zoals type="email", pattern en required om de ingebouwde validatielogica van de browser te activeren.
  2. Visuele feedback: Gebruik :user-invalid om foutstijlen alleen na interactie toe te passen.
  3. Positieve bekrachtiging: Gebruik eventueel :user-valid om een groene succesindicator te tonen als aan de vereisten is voldaan.
  4. Herstel: Zodra de gebruiker de invoer corrigeert naar een geldige indeling, stopt :user-invalid met overeenkomen en wordt de foutstatus onmiddellijk verwijderd.

Implementatiehandleiding

Use case 1: E-mailvalidatie

VERPLICHT: Vertrouw op standaard HTML5-kenmerken voor e-mailvelden. De foutmelding is standaard verborgen en wordt alleen getoond als de browser vaststelt dat de gebruiker het veld in een ongeldige status heeft verlaten.

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

Use case 2: Complexiteit van wachtwoorden

VERPLICHT: Definieer de complexiteitsregel met een Regex Lookahead-patroon in het kenmerk pattern. De lijst met regels wordt boven de invoer getoond om de gebruiker te helpen en wordt gemarkeerd als er een fout is.

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

Terugval en browserondersteuning

Fallbacks en browserondersteuning voor :user-valid en :user-invalid

CSS voor reserve

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-reserve

Gebruik een herbruikbaar hulpprogramma dat de interactiestatus bijhoudt met een WeakMap. Zo voorkomt u dat het DOM wordt vervuild met 'vuile' klassen of gegevenskenmerken.

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

Andere overwegingen

  1. Toegankelijkheid:
    • VERPLICHT: Gebruik aria-describedby om de lijst met regels aan de invoer te koppelen.
    • NIET DOEN: Verberg de lijst met regels niet helemaal totdat de invoer geldig is. Gebruikers moeten weten wat ze moeten typen.
  2. Limieten voor patroonkenmerken: VERPLICHT: Het kenmerk pattern voert een volledige overeenkomst uit (impliciet ^...$). Zorg dat de regex van je wachtwoord rekening houdt met de hele tekenreeks.
  3. Striktheid van validatie: Houd er rekening mee dat de standaard type="email"-validatie van de browser vrij tolerant is (user@localserver kan bijvoorbeeld worden goedgekeurd). Als u een strengere validatie nodig heeft, moet u misschien een robuustere validatiebibliotheek of een aangepaste validatiefunctie gebruiken naast type="email".
  4. Focusbeheer: VERPLICHT: Als een gebruiker het formulier indient met een ongeldig veld, richt de browser zich automatisch op het eerste ongeldige veld. Je :user-invalid-stijlen worden meteen toegepast, omdat een indieningspoging telt als een interactie.
  5. Consistente ARIA-functionaliteit: Systeemeigen :user-invalid wordt niet automatisch gesynchroniseerd met ARIA-kenmerken. Voeg de volgende JavaScript toe om aria-invalid gesynchroniseerd te houden met de visuele status:
// 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);
});