Form Validation Styles
HTML5 form controls carry built-in validation state that CSS can style directly — no JavaScript required to know whether a field is currently valid. But styling that state well means being careful about when it appears: showing red, angry borders on every field the instant a form loads (before the user has typed anything) is a real, common anti-pattern that this page explains how to avoid.
:valid and :invalid
The :valid and :invalid pseudo-classes (covered in depth on the Form State Pseudo-Classes page) match a form control the instant its value does or doesn't satisfy its constraints — required, type="email", pattern, min/max, and so on.
CSS
input:invalid {
border-color: #d62828;
}
input:valid {
border-color: #2a9d8f;
}Warning
Applied naively, this is jarring: an empty
required field is :invalid from the moment the page loads, so a form can appear covered in red errors before the user has typed a single character. This trains users to distrust your validation styling and feels hostile on a form they haven't even started filling out.:user-invalid and :user-valid
The :user-invalid and :user-valid pseudo-classes solve exactly this problem. They match the same validity state as :invalid/:valid, but only after the user has actually interacted with the field — typically after a change of value followed by the field losing focus. Until then, no validation styling is applied at all, so an untouched required field stays visually neutral.
CSS
/* Before: validation styling appears immediately, even on untouched fields */
input:invalid {
border-color: #d62828;
background: #fdecea;
}
/* After: validation styling only appears once the user has interacted */
input:user-invalid {
border-color: #d62828;
background: #fdecea;
}
input:user-valid {
border-color: #2a9d8f;
background: #eafaf3;
}Worked example: before vs after
CSS
<form>
<label for="email">Email</label>
<input type="email" id="email" required />
<span class="error-text">Enter a valid email address.</span>
</form>
/* Naive version: every required field is red on page load */
.naive input:invalid {
border-color: #d62828;
}
.naive input:invalid + .error-text {
display: block;
}
/* Better version: neutral until the user actually types and moves on,
then reflects real-time validity */
input {
border: 2px solid #ccc;
}
input:user-invalid {
border-color: #d62828;
}
input:user-valid {
border-color: #2a9d8f;
}
.error-text {
display: none;
color: #d62828;
font-size: 0.875rem;
}
input:user-invalid + .error-text {
display: block;
}Native validation bubbles are largely unstylable
When a form fails native constraint validation on submit, browsers show their own little popup message (often called a "validation bubble") anchored to the offending field. Historically some engines exposed a pseudo-element like
::-webkit-validation-bubble for this, but support and styling control have always been extremely limited and inconsistent across browsers.Note
Because native validation bubbles can't be reliably restyled to match your design, most production teams don't try. The common approach is to suppress the native bubble (for example by handling
invalid events and calling event.preventDefault(), or using novalidate on the form) and render your own validation message markup — styled with ordinary CSS, driven by :user-invalid/:invalid — that you have full control over.Next
See the full set of state-based selectors on [Form State Pseudo-Classes](/css/state-pseudo), or continue with [appearance property](/css/appearance-property) to reshape the controls themselves.