Show Form Errors After Interaction With :user-invalid
:invalid is accurate and often impolite. A required field is invalid before the user has touched it, so styling that selector can make a new form open with a collection of red borders.
:user-invalid and :user-valid let the browser wait for user interaction before exposing those states to CSS.
Keep the initial field neutral
<label for="email">Email</label>
<input id="email" name="email" type="email" required>
This selector may match as soon as the empty field is rendered:
input:invalid {
border-color: red;
}
Use the user-interaction state instead:
input {
border: 1px solid #aaa;
}
input:user-invalid {
border-color: #b00020;
}
Exactly when the state becomes active is controlled by the browser’s interaction rules. It is meant to avoid presenting untouched fields as user errors, not to provide a universal “blurred once” event in CSS.
Native constraints do the checking
These pseudo-classes reflect HTML constraint validation:
<input type="email" required>
<input type="text" minlength="3" required>
<input type="number" min="1" max="10">
No JavaScript is needed for those basic rules. JavaScript is still useful for validation that depends on a server or several fields, and for controlling when custom messages appear.
Colour is not an error message
A red border can support an error, but it cannot explain one. Put useful text near the field and associate it with the control:
<label for="email">Email</label>
<input
id="email"
type="email"
required
aria-describedby="email-error"
>
<p id="email-error">Enter a valid email address.</p>
Do not display that paragraph unconditionally in a real form; toggle it in a way that matches the validation behaviour. The point is that the user needs more than a colour code.
:has() can extend the state to a wrapper:
.field:has(input:user-invalid) {
color: #b00020;
}
Keep focus visible
input:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
}
A field can be focused and invalid at the same time. Both signals should survive. I also rarely add green borders to every valid input; silence is a perfectly good success state for most forms.
Use native constraints where they fit, show errors when they become useful, and explain how to fix them. The newer selectors handle the timing more gracefully, but they do not replace good error copy.