Feedback over verplichte velden

Het probleem

Het kan verwarrend zijn als verplichte velden meteen bij het laden van de pagina een foutstatus krijgen. In het ideale geval ziet een verplicht veld er alleen ongeldig uit als de gebruiker heeft geprobeerd het in te vullen en dat is mislukt.

De oplossing

De pseudoklasse :user-invalid lost dit perfect op. Voor een verplicht veld is er geen overeenkomst als de pagina wordt geladen. Er is alleen een overeenkomst als:

  1. De gebruiker voert een actie uit in het veld (bijvoorbeeld een teken typen en verwijderen) en verlaat het veld (vervagen), waardoor het leeg blijft.
  2. De gebruiker probeert het formulier in te dienen terwijl het veld leeg is.

Implementatiestrategie

  1. HTML-beperking: Voeg het kenmerk required toe aan je invoer.
  2. Visuele feedback: Gebruik :user-invalid om de rand rood te maken en de hulptekst Vereist te tonen.
  3. Timing: Vertrouw op de eigen timing van de browser voor visuele feedback. Je hebt geen onBlur-handlers meer nodig om een touched-klasse toe te voegen, hoewel er nog steeds wat JavaScript nodig is om ARIA-kenmerken te synchroniseren (zie hieronder).

Implementatiehandleiding

1. HTML-structuur

<form id="feedback-form">
  <div class="field">
    <label for="full-name">Full Name</label>
    <input
      type="text"
      id="full-name"
      name="full-name"
      required
      aria-errormessage="name-error"
    >
    <!-- MANDATORY: Include an icon or distinct non-color indicator alongside error text -->
    <div id="name-error" class="error-msg">
      <span aria-hidden="true">❌</span> This field is required.
    </div>
  </div>
</form>

2. CSS

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

/*
  Only highlight empty required fields AFTER the user visits them.
  MANDATORY: Provide multiple indicators (border shift + helper text/icon) to avoid color-only state communication.
*/
input:user-invalid {
  border-color: #d93025;
  background-color: #fce8e6;
}

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

/* Optional: Subtle indicator for required fields that are valid */
input:required:user-valid {
  border-color: #188038;
  border-width: 2px;
}

3. JavaScript-statussynchronisatie

VERPLICHT: Omdat :user-invalid een visuele status is, MOET u een JavaScript-bridge bieden om aria-invalid="true" dynamisch te synchroniseren voor ondersteunende technologieën als een gebruiker een ongeldig veld vervaagt of probeert een formulier in te dienen.

const form = document.getElementById('feedback-form');

const syncAriaInvalid = (input) => {
  if (!input.checkValidity()) {
    input.setAttribute('aria-invalid', 'true');
  } else {
    input.removeAttribute('aria-invalid');
  }
};

// Sync on blur when a user finishes interacting
form.addEventListener('blur', (e) => {
  if (e.target.matches('input[required]')) {
    syncAriaInvalid(e.target);
  }
}, true);

// Sync all required fields when submission is attempted
form.addEventListener('submit', () => {
  form.querySelectorAll('input[required]').forEach(syncAriaInvalid);
});

// Remove error state immediately upon correction
form.addEventListener('input', (e) => {
  if (e.target.matches('input[required]') && e.target.checkValidity()) {
    e.target.removeAttribute('aria-invalid');
  }
});

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. Asterisken: Het is nog steeds een best practice om verplichte velden visueel aan te geven (bijvoorbeeld met een asterisk *) in het label, zodat gebruikers weten wat ze kunnen verwachten voordat ze interactie hebben.
  2. Knoppen Verzenden: In tegenstelling tot disabled-knoppen moet u de knop Verzenden aan laten staan. Als de gebruiker erop klikt, activeert de browser automatisch :user-invalid voor alle lege verplichte velden en wordt het eerste veld geselecteerd. Dit is uitstekend voor de toegankelijkheid en UX.
  3. Toegankelijkheid: 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);
});