Zum Inhalt springen
Formulare

Formulare validieren mit CSS statt mit einer Bibliothek

Warum :invalid das falsche Werkzeug ist, wie :user-invalid den richtigen Zeitpunkt trifft, wohin die Fehlermeldung gehört, was danach für JavaScript übrig bleibt und wie das neue Select und field-sizing dazu passen.

10 Min. LesezeitVon Leonie RoesmannStand: 02. November 2026
FormulareValidierungCSSBarrierefreiheit

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.

Das Wichtigste in Kürze
  • :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.
01

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.

02

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;}
Dieselbe Regel, zwei Zeitpunkte. Die untere Fassung meldet sich erst, wenn jemand das Feld benutzt hat.
Die Farbe ist nicht die MeldungEin roter Rand sagt, dass etwas nicht stimmt, aber nicht was. Für Menschen, die Farben schlecht unterscheiden, sagt er auch das Erste nicht. Jede Markierung braucht deshalb einen Satz daneben, und der Satz braucht einen Inhalt, der weiterhilft: nicht Ungültige Eingabe, sondern die Angabe, was erwartet wird und in welcher Form.
03

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.

  1. 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.
  2. 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.
  3. 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.
NameE-MailAbsenden
Tabbewegt den Ring, focus-visible macht ihn sichtbar.
Drei Felder, ein Ring. Was die Tastatur erreicht, ist dieselbe Reihenfolge, in der die Meldungen erscheinen.
<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; }
Die Meldung steht im Dokument und erscheint zum selben Zeitpunkt wie der rote Rand.
04

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);
Eine eigene Regel, in dieselbe Mechanik eingehängt. Das Zurücksetzen gehört dazu.
Nachgebaut
  • 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.
Abgefragt
  • 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.
05

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;}
Das Menü des Auswahlfeldes und ein Textfeld, das mit dem Inhalt wächst. Beides ohne Skript.
Reihenfolge statt AbfrageKeine dieser drei Angaben braucht eine Abfrage mit @supports. Sie ersetzen nichts, sie legen etwas obendrauf: Ein Browser ohne base-select zeigt das übliche Auswahlfeld, eines ohne field-sizing ein Textfeld mit fester Zeilenzahl. Schreib deshalb zuerst die Fassung, die überall trägt, und danach die Zugabe. Eine Abfrage braucht nur, wer die Gestaltung auf die Zugabe stützt, etwa wenn ein eigenes Symbol nur dann sichtbar sein soll, wenn der Browser das eingebaute ausblendet.
06

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.

Teilen
Geschrieben von
Leonie RoesmannDesign Engineering

Wir bauen Websites und Web-Apps bei Systra Studios in Münster und legen die Bausteine daraus hier ab. Was in den Beiträgen steht, kommt aus echten Projekten, nicht aus einem Werbeprospekt.

Systra Studios: Webdesign aus Münster

Die Prüfung steht im Stylesheet.
Das Formular drumherum steht bereit.

Vom kurzen Kontaktfeld bis zum Formular mit Karte daneben: fertige Sections mit Beschriftung über dem Feld, sichtbarem Fokus und Platz für die Meldung unter jedem Feld. Kopieren, Felder anpassen, weiterbauen.

  • Beschriftung über dem Feld, nicht als Platzhalter darin.
  • Eine Datei je Komponente, nur react, framer-motion und lucide-react.
  • Reduzierte Bewegung ist in jeder Komponente berücksichtigt.
Formulare ansehen
FAQ

Häufige Fragen

Nach einer Eingabe und dem Verlassen des Feldes, oder nach einem Absendeversuch. Die Spezifikation beschreibt den Unterschied zu :invalid über die Beteiligung der Person: :invalid beschreibt den Zustand des Wertes, :user-invalid beschreibt den Zustand des Wertes, nachdem jemand ihn angefasst hat. Ein leeres Pflichtfeld beim Laden der Seite ist also ungültig, aber nicht benutzerungültig. Tippt jemand etwas Unvollständiges hinein und geht mit Tab weiter, greift die Pseudo-Klasse. Danach wird bei jedem Tastendruck neu geprüft, die Markierung verschwindet also sofort wieder, sobald der Wert passt. Ein Klick auf den Absenden-Knopf hat denselben Effekt für alle Felder auf einmal, auch für die, die niemand berührt hat. Genau diese drei Zeitpunkte sind die, die man in einer eigenen Lösung sonst mit Listenern auf blur, input und submit nachbaut.

Für ein Kontakt-, Anmelde- oder Bestellformular reicht sie in den meisten Fällen, für ein mehrstufiges Formular mit Abhängigkeiten zwischen Feldern nicht. Die Trennlinie verläuft dort, wo eine Regel mehr als ein Feld kennt. required, type, minlength, maxlength, min, max, step und pattern prüft der Browser selbst, inklusive Meldung und Fokus auf das erste fehlerhafte Feld. Sobald eine Regel lautet, dass das Enddatum nach dem Startdatum liegen muss, oder dass ein Feld nur dann Pflicht ist, wenn ein anderes angekreuzt wurde, braucht es Code, und dafür gibt es setCustomValidity. Der Unterschied zur Bibliothek bleibt trotzdem groß: Statt eines Pakets, das auch Zustände, Zeitpunkte und Meldungen mitbringt, bleiben einige Zeilen für genau die Regeln, die wirklich eigen sind. Und egal, wie viel Code dazukommt: Die Prüfung auf dem Server ersetzt nichts davon, denn alles im Browser lässt sich umgehen.

Mit den Varianten user-valid und user-invalid, die Tailwind seit Version 4 mitbringt. Eine Klasse wie user-invalid:border-red-600 setzt die Farbe genau dann, wenn die Pseudo-Klasse greift, und user-valid:border-emerald-600 die Gegenprobe. Für die Meldung unter dem Feld braucht es einen Weg vom Feld zum Geschwisterelement, und dafür gibt es zwei: die Variante peer, wenn die Meldung nach dem Feld steht, also peer-user-invalid:block auf dem Absatz, oder die Variante has auf einem umgebenden Element, also has-user-invalid gefolgt von der Klasse, wenn mehr als ein Element reagieren soll. Beides bleibt im Markup und kommt ohne eigene CSS-Datei aus. Was dort nicht hingehört, ist die Meldung des Browsers selbst: Sie ist eine Blase außerhalb des Dokuments und nimmt keine Klassen an.

Nur, wenn du jede Meldung selbst in die Seite schreibst, sonst nicht. Das Attribut novalidate am Formular schaltet die automatische Prüfung beim Absenden ab, nicht aber die Pseudo-Klassen und nicht die Methoden checkValidity und reportValidity. Wer es setzt, übernimmt damit drei Dinge: das Verhindern des Absendens, das Setzen des Fokus auf das erste fehlerhafte Feld und die Ansage für Screenreader. Der Grund, es trotzdem zu tun, ist meist die Gestaltung der Blase, die sich nicht gestalten lässt und in jedem Browser anders aussieht. Der ehrliche Weg führt deshalb über die Reihenfolge: zuerst die Meldungen sichtbar in die Seite schreiben, mit aria-describedby am Feld und aria-live am Bereich, und erst danach novalidate setzen. Umgekehrt entsteht ein Formular, das stumm nichts tut.

Deine Frage war nicht dabei? Wir helfen gern weiter.

Kontakt aufnehmen