Show validation feedback only once the user has actually done something
The classic :valid and :invalid pseudo-classes fire as soon as a form field exists, often before the first keystroke. That leads to red borders around empty required fields right when the page loads. The newer :user-valid and :user-invalid pseudo-classes instead wait for real user interaction and solve this problem entirely without JavaScript.
Table of Contents
- 1. The problem: an error state before the user has done anything
- 2. A quick refresher: what :valid and :invalid actually check
- 3. What specifically triggers :user-invalid
- 4. :user-valid as the symmetric counterpart
- 5. The difference side by side: timing of when it fires
- 6. Practical example: a registration form without premature errors
- 7. Combining with :required, :optional and the placeholder trick
- 8. Comparison to the classic JavaScript approach
- 9. Browser support and a safe fallback
- 10. Summary
- 11. FAQ
1. The problem: an error state before the user has done anything
A form with an email field marked required counts as invalid from the browser's perspective as long as it stays empty. If that state is styled directly with the :invalid pseudo-class, the red error border shows up right when the page loads, even though the user has not even had the chance to fill in the field yet. That feels accusatory rather than helpful.
This behavior is not a bug, it follows the specification exactly: :invalid describes a pure state, regardless of how it came about. For years the only workaround was setting custom classes with JavaScript after a blur or submit event and ignoring the actual CSS pseudo-class. That is exactly the gap :user-valid and :user-invalid close natively.
2. A quick refresher: what :valid and :invalid actually check
The :valid and :invalid pseudo-classes reflect the result of HTML's built-in constraint validation: they take attributes like required, pattern, min, max, type and minlength/maxlength into account, with no custom JavaScript required. A field of type email holding a value without an @ character, for example, automatically counts as :invalid.
This built-in validation is powerful because it emerges declaratively straight from the markup, but has exactly the one weakness described in the previous section: it does not distinguish between a field the user has already touched and one that has stayed untouched since the page loaded. Both count identically as :invalid.
3. What specifically triggers :user-invalid
The :user-invalid pseudo-class applies, on top of the base condition of :invalid, only once the user has already meaningfully interacted with the field. Browsers interpret this slightly differently depending on the field type, but agree on the core idea: a text field typically counts as touched once the user has focused it, entered something, and then moved focus away, in other words a blur after a real change.
On top of that, most browsers also trigger :user-invalid when the user attempts to submit the form while a field is invalid, even if that field was never focused at all. This second condition ensures that a completely overlooked required field still gets visibly marked as invalid on submission, instead of staying unnoticed forever.
4. :user-valid as the symmetric counterpart
Analogously, :user-valid only applies to fields that are valid and that the user has already interacted with. This is especially useful for giving positive feedback, such as a green checkmark next to a correctly filled-in password field, without that checkmark already appearing on a completely empty, technically valid optional field.
Without this interaction condition, :valid would apply immediately to every optional, empty field, because an empty field not marked required is valid by definition. A green border around a field the user has not even seen yet would be just as confusing as the red border from the introductory example, only with the opposite sign.
5. The difference side by side: timing of when it fires
The central difference between the two pseudo-class pairs lies purely in timing, not in the checked state itself: :invalid only checks whether constraint validation is currently failing, :user-invalid additionally checks whether the user has already had a chance to fix it. A field can therefore be technically invalid while not yet being :user-invalid, namely right after the page loads.
Once the user leaves the field without filling it in correctly, both pseudo-classes apply simultaneously from that moment on. If the user afterward deletes their incorrect input and makes the field empty again, :user-invalid stays active in most implementations, because the interaction has already happened and is not undone.
6. Practical example: a registration form without premature errors
The following example shows a simple registration form where only :user-invalid and :user-valid handle the color feedback. On first load, not a single field turns red, even though both fields are marked required and currently empty.
Only once the user leaves a field without filling it in correctly, or attempts to submit the form with a still-empty required field, does the border turn red and an accompanying error message becomes visible through :user-invalid ~ .error-text, without a single JavaScript event listener.
input:user-invalid {
border-color: #dc2626;
background-color: #fef2f2;
}
input:user-invalid ~ .error-text {
display: block;
color: #dc2626;
font-size: 0.875rem;
}
input:user-valid {
border-color: #16a34a;
background-color: #f0fdf4;
}
/* Before any interaction, the field stays neutral regardless of constraint status */
input:not(:user-invalid):not(:user-valid) {
border-color: #d1d5db;
}
7. Combining with :required, :optional and the placeholder trick
For more nuanced states, :user-invalid can be combined with :required to treat required fields more strictly than optional ones, or with :optional to show no red marking at all on voluntary fields, even when an entered format is invalid, only a subtle hint color instead.
In forms that additionally use a floating label effect through :placeholder-shown, the same selector logic can be combined with :not(:placeholder-shown) to make sure an error state never appears at the same time as the floating placeholder label, which would otherwise visually overlap.
8. Comparison to the classic JavaScript approach
Before :user-invalid, the usual approach was setting custom CSS classes like .touched or .was-validated through JavaScript on blur and submit, and either disabling native constraint validation entirely or writing parallel custom check functions. That worked but tied form logic firmly to JavaScript, making forms completely non-functional in JavaScript-free contexts like AMP pages or with JavaScript disabled.
With :user-invalid, the browser handles exactly this interaction detection natively, which not only saves the JavaScript code but also guarantees consistency with the browser's own validation logic. For most standard forms, the pure CSS solution today covers the entire use case, with JavaScript only still needed for asynchronous checks like a server-side username availability check.
9. Browser support and a safe fallback
Both :user-valid and :user-invalid are now supported by all current versions of Chrome, Firefox and Safari, after support was rolled out gradually over several years. For older browser versions that do not yet know these pseudo-classes, the selector in the CSS simply has no effect, without causing an error, which provides low-risk progressive enhancement.
Anyone wanting extra safety for older browsers combines :user-invalid as the preferred selector with a deliberate feature detection through @supports selector(:user-invalid), so older browsers fall back instead to a somewhat coarser but still functional solution using :invalid:not(:placeholder-shown).
| Pseudo-class | Fires on | Fires before interaction |
|---|---|---|
:invalid |
Constraint validation fails | Yes, immediately on load |
:valid |
Constraint validation passes | Yes, immediately on load |
:user-invalid |
Invalid AND already touched or submit attempted | No |
:user-valid |
Valid AND already touched | No |
:invalid:not(:placeholder-shown) |
Invalid AND field no longer empty | No, but less precise than :user-invalid |
Mironsoft
Modern CSS, layout architecture and rendering performance
CSS that stays maintainable instead of breaking with every change?
We review existing stylesheets for specificity chaos and layout thrashing, then build a CSS architecture with cascade layers, custom properties and modern layout primitives that still makes sense after the tenth feature.
CSS Audit
Systematically uncovering specificity issues, cascade conflicts and unused selectors.
Architecture Refactoring
Introducing cascade layers, custom properties and design tokens cleanly.
Performance Tuning
Fixing layout thrashing, expensive selectors and rendering bottlenecks.
10. Summary
:user-valid and :user-invalid: The Essentials at a Glance
Core problem solved
:invalid fires immediately on load, :user-invalid waits for real user interaction or a submit attempt.
Two triggers
:user-invalid applies after a blur with a change, or when the user tries to submit despite an invalid field.
No JavaScript needed
Fully replaces the earlier practice of setting custom touched classes through blur/submit listeners.
Broad browser support
All current versions of Chrome, Firefox and Safari already support both pseudo-classes.