Die meisten Formulare melden ihre Fehler zum falschen Zeitpunkt. Entweder steht alles rot, bevor jemand ein einziges Zeichen getippt hat, weil :invalid ein leeres Pflichtfeld schon beim ersten Zeichnen der Seite trifft. Oder es steht gar nichts, bis jemand absendet, und dann springt die Seite zu einer Sammelmeldung, die niemand liest. Dazwischen liegt die Fassung, die beides vermeidet, und sie braucht keine Bibliothek: :user-invalid wartet, bis jemand das Feld tatsächlich benutzt und wieder verlassen hat, und meldet sich erst dann. Die Pseudo-Klasse gibt es in Firefox seit April 2021, in Safari seit 16.5 im Mai 2023 und in Chrome seit 119 im Oktober 2023; seit dem 2. Mai 2026 führt Baseline sie als weithin verfügbar. Benutzt wird sie laut Chrome Platform Status auf rund 0,2 Prozent aller Seitenaufrufe. Dieser Beitrag zeigt, was sie abnimmt, wohin die Meldung gehört, was danach für JavaScript übrig bleibt und wie das neue Select und field-sizing in dasselbe Formular passen.
- :invalid und :user-invalid prüfen dieselbe Regel, aber zu verschiedenen Zeitpunkten. :invalid gilt ab dem ersten Zeichnen, :user-invalid erst nach Eingabe und Verlassen des Feldes oder nach einem Absendeversuch.
- Der Zustand läuft danach von selbst mit. Sobald ein Feld einmal gemeldet wurde, prüft der Browser bei jedem Tastendruck nach und nimmt die Markierung weg, sobald der Wert stimmt.
- Die Farbe allein ist keine Meldung. Der Satz, der sagt, was fehlt, gehört sichtbar unter das Feld und über aria-describedby an das Feld gebunden.
- Für JavaScript bleibt wenig: eigene Fehlertexte über setCustomValidity, Prüfungen, die der Browser nicht kennt, und das Verhalten beim Absenden. Die Zeitpunkte und die Zustände kommen aus CSS.
Warum ist mein Formular rot, bevor jemand getippt hat?
Weil :invalid den Zustand eines Wertes beschreibt und nicht den Stand eines Gesprächs. Ein Pflichtfeld ohne Inhalt ist ungültig, und zwar von dem Moment an, in dem die Seite gezeichnet wird. Die Regel stimmt, der Zeitpunkt nicht: Niemand hat etwas falsch gemacht, trotzdem meldet die Oberfläche einen Fehler.
Die übliche Antwort darauf ist eine Umgehung. Das Formular bekommt eine Klasse, die erst beim ersten Absendeversuch gesetzt wird, und alle Regeln hängen daran. Oder jedes Feld bekommt einen Listener auf blur, der eine Klasse setzt, und einen zweiten auf input, der sie wieder wegnimmt. Beides funktioniert, beides ist Zustandsverwaltung für etwas, das der Browser bereits weiß.
Die Gegenrichtung ist genauso verbreitet und genauso unangenehm: gar keine Rückmeldung, bis jemand absendet. Baymard gibt an, dass 31 Prozent der untersuchten Seiten ohne Prüfung während des Ausfüllens auskommen. Wer erst am Ende erfährt, dass die Postleitzahl zu kurz ist, muss zurückscrollen und sich erinnern, was dort stand.
Die Regel war nie das Problem. Der Zeitpunkt war es.
Was macht :user-invalid anders als :invalid?
Sie nimmt die Beteiligung der Person mit in die Bedingung. :user-invalid trifft ein Feld erst, wenn dessen Wert verändert und das Feld verlassen wurde, oder wenn jemand abgesendet hat. Vorher trifft sie nichts, auch wenn der Wert nach den Regeln falsch ist. Dieselbe Logik in die Gegenrichtung heißt :user-valid und eignet sich für das Häkchen, das bestätigt, dass eine Eingabe angekommen ist.
Damit entfallen genau die drei Listener, die man sonst schreibt. Der erste Treffer kommt beim Verlassen des Feldes, die Korrektur wird beim Tippen sofort erkannt, und ein Absendeversuch markiert auf einen Schlag alles, was fehlt. Nichts davon muss gespeichert, zurückgesetzt oder beim erneuten Rendern wiederhergestellt werden, denn es ist kein Zustand im Code, sondern eine Abfrage im Stylesheet.
Die Unterstützung ist kein Hindernis mehr. Firefox kennt die Pseudo-Klasse seit Version 88 im April 2021, Safari seit 16.5 im Mai 2023, Chrome und Edge seit Version 119 Anfang November 2023. Damit war sie Ende 2023 in allen Engines vorhanden und gilt seit dem 2. Mai 2026 als weithin verfügbar. Trotzdem taucht sie laut Chrome Platform Status nur auf etwa 0,2 Prozent der Seitenaufrufe auf, ungefähr ein Zehntel so oft wie field-sizing, das erst zwei Jahre später fertig wurde.
/* red from the first paint, before anyone typed */input:invalid { border-color: #c0392b;} /* red once the field was used and left */ input:user-invalid { border-color: #c0392b;} input:user-valid { border-color: #1f8a4c;}
Wohin gehört die Fehlermeldung im Markup?
Unter das Feld, im Dokument, und am Feld angemeldet. Die Blase, die der Browser beim Absenden zeigt, erfüllt das nicht: Sie steht außerhalb des Dokuments, verschwindet nach wenigen Sekunden, lässt sich nicht gestalten und zeigt immer nur einen Fehler auf einmal. Als Rückfallebene ist sie gut, als einzige Rückmeldung nicht.
Die eigene Meldung braucht drei Dinge. Sie steht als Element nach dem Feld, damit CSS sie vom Feld aus erreichen kann. Sie hängt über aria-describedby am Feld, damit ein Screenreader sie zusammen mit der Beschriftung vorliest. Und sie ist nur sichtbar, wenn sie zutrifft, gesteuert über dieselbe Pseudo-Klasse, die auch den Rand färbt.
Der Weg vom Feld zur Meldung geht mit dem Geschwister-Kombinator, der Weg zum ganzen Feldblock mit :has. Letzteres braucht man, sobald mehr als ein Element reagieren soll, etwa wenn der Hinweis zum Format verschwinden soll, sobald die Fehlermeldung erscheint. Beides steht seit Dezember 2023 in allen großen Browsern zur Verfügung.
- Die Beschriftung bleibt sichtbar. Ein Platzhalter als Beschriftung verschwindet beim Tippen, genau dann, wenn man ihn zum Prüfen bräuchte. Die WebAIM-Auswertung von einer Million Startseiten nennt für 2026 fehlende Beschriftungen bei 51 Prozent der Seiten und 33,1 Prozent der Felder.
- Der Text sagt die Form. Statt Ungültiges Datum besser Als Tag, Monat, Jahr, zum Beispiel 04.11.2026. Wer die erwartete Form nennt, erspart den zweiten und dritten Versuch.
- Die Meldung ist keine Überraschung. Ein Bereich mit aria-live auf polite sagt Änderungen an, ohne das Vorlesen zu unterbrechen. Für den Absendeversuch reicht meist der Fokus auf das erste fehlerhafte Feld, den der Browser ohne novalidate von selbst setzt.
<div class="field"> <label for="mail">Email</label> <input id="mail" type="email" required aria-describedby="mail-error mail-hint" /> <p id="mail-error" class="error"> We need a full address, like name@studio.com </p> <p id="mail-hint" class="hint">Only used for the receipt.</p></div> .error { display: none; color: #c0392b; } input:user-invalid ~ .error { display: block; } .field:has(input:user-invalid) .hint { display: none; }
Was bleibt danach für JavaScript übrig?
Drei Dinge, und keines davon ist Zustandsverwaltung. Das erste sind Regeln, die der Browser nicht kennt, weil sie mehr als ein Feld betreffen oder eine Antwort vom Server brauchen. Das zweite sind eigene Texte für Regeln, die er kennt, deren Standardmeldung aber nicht zum Formular passt. Das dritte ist das Verhalten beim Absenden, wenn das Formular nicht neu lädt.
Für die ersten beiden gibt es setCustomValidity. Die Methode schreibt einen Text in die Gültigkeitsmeldung des Feldes, und solange dieser Text nicht leer ist, gilt das Feld als ungültig. Damit greifen :invalid und :user-invalid auf eigene Regeln genauso wie auf eingebaute, und die Gestaltung bleibt dieselbe. Wichtig ist nur das Zurücksetzen: Ohne einen Aufruf mit einer leeren Zeichenkette bleibt das Feld für immer ungültig.
Beim Absenden übernimmt der Browser mehr, als die meisten annehmen. Ohne novalidate prüft er alle Felder, verhindert das Absenden, setzt den Fokus auf das erste fehlerhafte Feld und zeigt dessen Meldung. Wer selbst absendet, etwa mit fetch, braucht dafür checkValidity als Bedingung und reportValidity, wenn die Meldungen des Browsers erscheinen sollen.
const end = form.elements.end; const checkRange = () => { const tooEarly = end.value && end.value < form.elements.start.value; end.setCustomValidity(tooEarly ? 'End date comes after the start date.' : '');}; end.addEventListener('input', checkRange);form.elements.start.addEventListener('input', checkRange);
- Ein Listener auf blur setzt eine Klasse touched, ein zweiter auf input nimmt sie weg.
- Ein Objekt im Zustand hält pro Feld, ob es berührt wurde und ob es gerade fehlerhaft ist.
- Beim Absenden läuft eine Schleife über alle Felder und setzt alle Klassen von Hand.
- Nach einem Rendern mit neuen Werten sind die Klassen weg und müssen neu gesetzt werden.
- input:user-invalid trifft nach Eingabe und Verlassen, ohne eine Zeile im Skript.
- Der Browser prüft bei jedem Tastendruck nach und nimmt die Markierung selbst wieder weg.
- Ein Absendeversuch markiert alle fehlenden Felder auf einmal, auch die unberührten.
- Ein erneutes Rendern ändert nichts, weil der Zustand nirgends gespeichert liegt.
Wie passen das neue Select und field-sizing dazu?
Sie lösen die beiden anderen Gründe, aus denen Formulare nachgebaut werden. Der erste ist das Auswahlfeld, das sich bis vor kurzem nicht gestalten ließ und deshalb in fast jedem Projekt durch ein eigenes Menü aus div-Elementen ersetzt wurde, mit allem, was daran hängt: Tastatur, Ansage, Position am Rand des Bildschirms. Seit appearance mit base-select auf dem Feld und auf ::picker(select) steht, ist das Menü selbst gestaltbar, und ::checkmark und ::picker-icon sind die Teile darin.
Die Unterstützung ist hier jünger. Chrome hat es in Version 135 im April 2025 ausgeliefert, Safari in Version 27 im September 2026, Firefox bisher nicht. Das Verhalten ist dasselbe wie beim Karussell aus Marker und Knöpfen: Wo appearance nicht verstanden wird, bleibt das gewohnte Auswahlfeld stehen, und niemand sieht eine halbe Oberfläche. Es ist also eine Zugabe, keine Entscheidung, und laut State of HTML 2025 hatten 49,8 Prozent der Befragten noch nie davon gehört.
Der zweite Grund ist das Textfeld, das mitwachsen soll. Dafür gab es jahrelang ein Skript, das die Höhe misst und setzt, und das in jedem React-Projekt als Abhängigkeit landete. field-sizing mit content macht daraus eine Zeile. Chrome kann es seit Version 123 im März 2024, Safari seit 26.2 im Dezember 2025 und Firefox seit Version 152 im Juni 2026, womit es Baseline erreicht hat.
select,select::picker(select) { appearance: base-select;} select::picker(select) { border: 1px solid currentColor; border-radius: 12px; padding: 0.25rem;} option:checked::checkmark { color: #e03e10;} textarea { field-sizing: content; min-block-size: 4lh;}
Woran erkennt man, dass ein Formular fertig ist?
An vier Durchgängen, die zusammen eine Viertelstunde dauern und die in dieser Reihenfolge am meisten finden. Sie prüfen nicht die Regeln, sondern die Zeitpunkte, denn die Regeln stimmen meist und die Zeitpunkte selten.
Der erste Durchgang ist der leere: Seite laden, nichts anfassen, hinsehen. Nichts darf rot sein, kein Knopf darf ausgegraut sein, kein Hinweis darf fehlen. Der zweite ist der falsche: in jedes Feld etwas Unvollständiges tippen, mit Tab weitergehen und lesen, was dort steht. Jede Meldung muss die erwartete Form nennen, und keine darf erscheinen, bevor das Feld verlassen wurde.
Der dritte ist der ungeduldige: sofort absenden, ohne etwas auszufüllen. Alle fehlenden Felder müssen auf einmal markiert sein, der Fokus muss im ersten davon stehen, und die Seite darf nicht springen. Der vierte ist der ohne Maus: mit Tab durch das ganze Formular, Auswahlfelder mit den Pfeiltasten, absenden mit Enter. Wenn dabei der Ring immer sichtbar ist und die Reihenfolge der Felder der Reihenfolge im Bild entspricht, ist das Formular fertig.
Was danach übrig bleibt, ist der Teil, den kein Stylesheet abnimmt: die Frage, ob das Feld überhaupt gebraucht wird. Jedes Feld, das verschwindet, ist eine Validierung weniger, eine Meldung weniger und ein Grund weniger, abzubrechen. Die beste Fehlerbehandlung ist die, die nicht nötig wird.
Für den Rest gilt die Bilanz dieses Ansatzes: Was früher eine Bibliothek mit Zustandsobjekt war, sind jetzt zwei Pseudo-Klassen, ein Geschwister-Kombinator und ein paar Zeilen für die eigenen Regeln. Und der Zustand, der dabei am wenigsten Ärger macht, ist der, den niemand speichert.
Prüfe die Zeitpunkte, nicht die Regeln. Die Regeln stimmen meistens.






