CSS :invalid Pseudo Class
Learn how CSS :invalid styles form fields that fail validation constraints, with examples, gotchas, accessibility tips, and browser support.
The CSS :invalid pseudo-class matches form-associated controls — such as <input>, <select>, and <textarea> — whose current value fails the browser's built-in validation constraints. It is the visual half of HTML's constraint-validation API: the browser evaluates validity, and :invalid lets you style the result with pure CSS, no JavaScript required.
This page covers exactly when :invalid matches, how to style it without alarming users before they've typed anything, the radio-button quirk, the modern :user-invalid alternative, and how to keep error feedback accessible.
When does an element match :invalid?
A control matches :invalid whenever it carries at least one validation constraint and its current value violates that constraint. Common triggers:
| Constraint | Attribute / type | Fails when… |
|---|---|---|
| Required but empty | required | the field has no value |
| Wrong email format | type="email" | value isn't a syntactically valid e-mail |
| Wrong URL format | type="url" | value isn't a valid absolute URL |
| Out of range | min / max on type="number", type="date", etc. | value is outside the allowed range |
| Wrong step | step | value doesn't align with the step interval |
| Pattern mismatch | pattern | value doesn't match the regex |
| Too long / too short | minlength / maxlength | value length is outside the allowed range |
If a control has no constraints at all (a plain <input type="text"> with no extra attributes), it is considered unconstrained and neither :valid nor :invalid applies to it.
A <fieldset> element matches :invalid when any of its descendant form controls are invalid.
For the opposite side, see the :valid pseudo-class, which matches controls that pass all constraints. The :required pseudo-class matches required fields regardless of whether they have a value.
Syntax
:invalid {
/* declarations applied to all invalid form controls */
}Scope :invalid to a specific element type to keep styles predictable:
input:invalid,
textarea:invalid {
border: 2px solid #c00;
outline: none;
}Basic example
The email field below is pre-filled with a malformed address ("not-an-email"), so it matches input:invalid on load and gets a red border.
<!DOCTYPE html>
<html>
<head>
<title>:invalid example</title>
<style>
input:invalid {
border: 2px solid #c00;
background-color: #fff0f0;
}
input:valid {
border: 2px solid #090;
background-color: #f0fff0;
}
</style>
</head>
<body>
<h2>:invalid selector example</h2>
<form>
<label for="email">Email:</label>
<input id="email" type="email" value="not-an-email" required />
</form>
</body>
</html>Avoiding "premature red"
The most common :invalid pitfall: an empty required field is already invalid the moment the page loads, so a fresh form can light up red before the user has typed anything. That feels accusatory.
Option 1 — only style while the field is focused
Show the error border only while the user is actively in the field:
input:invalid:focus {
border-color: #c00;
outline: 2px solid #c00;
outline-offset: 1px;
}This is simple but disappears the moment the user tabs away, so a blank required field looks fine again.
Option 2 — hide the error while the placeholder is visible
:placeholder-shown is true when the placeholder text is displayed (i.e., the field is empty). Combining it with :not makes :invalid styling kick in only once the user has typed something:
/* Only show the error style when the field has a value that is invalid */
input:invalid:not(:placeholder-shown) {
border-color: #c00;
}This is effective but requires every field to have a placeholder attribute set — otherwise :placeholder-shown is never true and the guard does nothing.
Option 3 — use :user-invalid (modern standard)
The :user-invalid pseudo-class was designed specifically to solve this problem. It behaves like :invalid but only matches after the user has interacted with the control (typed in it, blurred it, or submitted the form):
/* Supported in all modern browsers as of 2024 */
input:user-invalid {
border-color: #c00;
}
/* Fallback for older browsers */
@supports not selector(:user-invalid) {
input:invalid:not(:placeholder-shown) {
border-color: #c00;
}
}:user-invalid is the cleanest solution when you can rely on it. Firefox has supported it as :-moz-ui-invalid for years; the standard :user-invalid is now in all modern browsers.
Styling approach
A harsh full border at 2 px red is readable but jarring. Consider combining a border change with a subtle box-shadow for a softer look:
input:invalid:not(:placeholder-shown) {
border-color: #c00;
box-shadow: 0 0 0 3px rgba(204, 0, 0, 0.15);
}Avoid relying on color alone — see the Accessibility section below.
Gotchas
Radio buttons
When a radio button group has required on one of its inputs, every button in the group matches :invalid while none is selected. Styling tiny radio circles isn't practical; instead, style the surrounding <fieldset> or <label>:
/* Style the fieldset, not the radio buttons themselves */
fieldset:invalid {
border: 2px solid #c00;
border-radius: 4px;
padding: 8px 12px;
}All radio buttons in a group share the same name attribute — that's what makes them a group in the browser's validity model.
Empty optional fields
A plain <input type="text"> with no required, no pattern, and no length constraints is always :valid even when empty. :invalid only fires when a constraint exists and is violated.
select and textarea
<select> matches :invalid if it is required and its current value is an empty string (a common pattern is a "-- choose --" placeholder <option value=""> at the top). <textarea> follows the same rules as <input> for required, minlength, and maxlength.
Firefox and :-moz-ui-invalid
Firefox has long applied styles via :-moz-ui-invalid, which only activates after user interaction — effectively the :user-invalid behavior built in. If you add your own :invalid rules and test in Firefox, the field may look fine (because the browser's default user-interaction guard is on), then behave differently in Chrome (where the default guard is off). Define explicit rules and use :user-invalid with a fallback to get consistent behavior.
Accessibility
Color alone is never sufficient for communicating an error — users with color-vision deficiencies may not perceive a red border. Pair :invalid styling with:
- A visible text message explaining what went wrong and how to fix it.
- An icon or symbol alongside the color change (e.g., a ✕ or warning icon).
aria-invalid="true"on the control so screen readers announce it as invalid.aria-describedbypointing at the error message element so the description is read automatically.
<label for="email">Email address</label>
<input
id="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
required
/>
<span id="email-error" role="alert">
Please enter a valid email address.
</span>The role="alert" on the error span causes screen readers to announce the message as soon as it appears in the DOM, even without focus.
Related pseudo-classes
| Pseudo-class | Matches when… |
|---|---|
:valid | the control passes all its constraints |
:required | the control has the required attribute |
:optional | the control does not have required |
:out-of-range | a numeric/date input's value exceeds min/max |
:in-range | a numeric/date input's value is within min/max |
:placeholder | the placeholder text of an input |
:focus | the control currently has keyboard focus |
Browser support
:invalid is part of Selectors Level 4 and has been supported in all major browsers for many years. :user-invalid (the interaction-aware variant) shipped in Chrome 119, Firefox 88 (as :-moz-ui-invalid much earlier), and Safari 16.5.
For more on HTML's native constraint attributes, see <input> and HTML Forms.