Barrierefreie Fehlermeldung

Das Problem

Die Standard-HTML5-Validierung bietet visuelles Feedback (über :invalid oder :user-invalid), wird aber nicht automatisch mit Attributen für Bedienungshilfen wie aria-invalid synchronisiert.

Wenn Sie das Standard-:invalid-Styling verwenden, kündigen Screenreader möglicherweise „Ungültiger Eintrag“ an, sobald ein Nutzer ein erforderliches Feld auswählt, das derzeit leer ist. Das ist für Nutzer, die Hilfstechnologien verwenden, störend, da der Fehler angekündigt wird, bevor eine Interaktion stattgefunden hat.

Die Lösung

Der programmatische Zustand (aria-invalid="true") soll genau dann angewendet werden, wenn der visuelle Zustand (:user-invalid) angewendet wird. Da :user-invalid auf dem internen „user-interacted“-Flag des Browsers basiert, können wir mit JavaScript prüfen, ob dieser Selektor bei Standardinteraktionsereignissen übereinstimmt.

Weitere Informationen finden Sie unter MDN aria-invalid.

Implementierungsstrategie

  1. Visuelle Ebene: Verwenden Sie CSS-:user-invalid, um Rahmen/Symbole anzuzeigen.
  2. Bedienungshilfenebene: Verwenden Sie aria-invalid und aria-errormessage, um den Status an Hilfstechnologien zu übermitteln.
  3. Visuelle und Barrierefreiheitsebene verbinden: Erstellen Sie ein einfaches JavaScript-Dienstprogramm, das auf blur- und input-Ereignisse wartet, prüft, ob das Element mit :user-invalid übereinstimmt, und die ARIA-Attribute entsprechend aktualisiert.

Implementierungsleitfaden

1. HTML-Struktur

Verknüpfen Sie die Eingabe mit der Fehlermeldung, indem Sie aria-errormessage (oder aria-describedby für eine breitere Unterstützung) verwenden.

<form>
  <div class="field">
    <label for="email">Email</label>
    <input 
      type="email" 
      id="email" 
      required 
      aria-errormessage="email-error"
    >
    <span id="email-error" class="error-msg">
      Please enter a valid email address.
    </span>
  </div>
</form>

2. CSS

Steuern Sie die Sichtbarkeit der Fehlermeldung mit der nativen Pseudoklasse :user-invalid.

.error-msg {
  display: none;
  color: #d93025;
}

/* Show error message when input is user-invalid */
input:user-invalid ~ .error-msg {
  display: block;
}

/* Optional: Visual cues on the input itself */
input:user-invalid {
  border-color: #d93025;
}

3. JavaScript

Da es kein „UserInvalidChanged“-Ereignis gibt, müssen Sie Standardformularereignisse verwenden, um den Status zu prüfen.

const updateAriaState = (event) => {
  const input = event.target;
  if (!input.matches?.('input, textarea, select')) return;

  // Check if the browser currently considers this input "user-invalid"
  const isUserInvalid = input.matches(':user-invalid');
  
  if (isUserInvalid) {
    input.setAttribute('aria-invalid', 'true');
  } else {
    input.removeAttribute('aria-invalid');
  }
};

// Listen on the document to handle dynamically added fields.
// 'blur' and 'focus' do not bubble, so we must use the capture phase (true).
document.addEventListener('blur', updateAriaState, true);
document.addEventListener('focus', updateAriaState, true);

// Also update on input if we've already shown the error, 
// so the error clears immediately when fixed.
document.addEventListener('input', (event) => {
  const input = event.target;
  if (!input.matches?.('input, textarea, select')) return;

  const hasAriaInvalid = input.hasAttribute('aria-invalid');
  const ariaInvalid = input.getAttribute('aria-invalid');
  if (hasAriaInvalid && ariaInvalid === 'true') {
    updateAriaState(event);
  }
});

Fallback und Browserunterstützung

Feature-Erkennung

Sie können die Unterstützung in CSS und JavaScript prüfen.

JavaScript-Prüfung:

if (!CSS.supports('selector(:user-invalid)')) {
  // Fallback logic here
}

CSS für Fallback

Damit Ihre Fallback-Logik visuell nicht vom nativen Verhalten zu unterscheiden ist, müssen Sie Ihre Fehlerstile sowohl auf die Pseudoklasse als auch auf die Fallback-Klasse anwenden.

/* Apply error styles to both native selector and fallback class */
input:user-invalid,
input.user-invalid-fallback {
  border-color: #d93025;
  background-color: #fce8e6;
}

/* Show error message for both cases */
input:user-invalid ~ .error-msg,
input.user-invalid-fallback ~ .error-msg {
  display: block;
}

Fallback-Logik

Wenn :user-invalid fehlt, erfassen Sie den Interaktionsstatus manuell mit einem WeakMap.

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' && input.matches?.('form')) {
      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.matches?.('input, textarea, select')) 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 = () => {
    if (CSS.supports('selector(:user-invalid)')) return;

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

  return { init };
})();

// Initialize globally
UserInvalidFallback.init();

Weitere Überlegungen

  1. aria-live im Vergleich zu aria-errormessage:

    • aria-errormessage verbindet die Eingabe mit dem Text, aber Screenreader kündigen sie möglicherweise nicht sofort an, wenn sie angezeigt wird (nur wenn der Fokus auf der Eingabe liegt).
    • Wenn Sie eine sofortige Ansage benötigen, wenn der Fehler auftritt (z. B. bei Unschärfe), können Sie role="alert" oder aria-live="polite" zum Container für die Fehlermeldung hinzufügen. Testen Sie dies jedoch gründlich, um eine „doppelte Ansage“ zu vermeiden, wenn der Nutzer das Feld fokussiert, um den Fehler zu beheben.
  2. Internationalisierung:

    • Achten Sie darauf, dass der Textinhalt Ihrer Fehlermeldung (#email-error) übersetzt ist. Die Logik bleibt gleich.