:user-valid and :user-invalid: Form Validation Without JavaScript
AI generated
{ }
@
CSS · Forms · Accessibility · UX
:user-valid and :user-invalid
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.

15 min read :user-valid · :user-invalid Form UX · Accessibility

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.

11. FAQ: :user-valid and :user-invalid: The Essentials at a Glance

1What is the main difference between :invalid and :user-invalid?
:invalid only checks the current validity status and fires regardless of any interaction. :user-invalid additionally only fires once the user has already touched the field or attempted to submit.
2Exactly when does a field count as touched for :user-invalid?
Typically once the user has focused the field, changed its value, and then moved focus away again. Additionally, a submit attempt with an invalid field also triggers the pseudo-class.
3Why shouldn't I use :invalid directly for red error markers?
Because :invalid already fires when the page loads, as soon as a required field is empty. That shows errors before the user has even had the chance to fill in the field.
4Is there a counterpart for valid fields?
Yes, :user-valid only applies to fields that are valid and that the user has already touched, ideal for positive feedback like a green checkmark.
5Does :user-invalid stay active if I empty the invalid field again?
In most implementations yes, because the interaction has already happened and is not undone. The field stays marked as user-invalid until correctly filled in.
6How do I combine :user-invalid with optional fields?
With :optional the selector can be specifically applied to or excluded from voluntary fields, to show a more subtle response there than on required fields.
7Does :user-invalid fully replace JavaScript form validation?
For the pure display logic of error states, yes. Asynchronous checks like a server-side username availability check still need JavaScript, but the result can still be hooked into native validation through setCustomValidity().
8Which browsers support :user-valid and :user-invalid?
All current versions of Chrome, Firefox and Safari already fully support both pseudo-classes, after support was rolled out gradually over several years.
9How do I safeguard older browsers without support?
With @supports selector(:user-invalid) you can check whether the browser knows the pseudo-class, and fall back for older browsers to :invalid:not(:placeholder-shown) as a coarser but still functional option.
10Does :user-invalid also work with custom form components?
Only with real HTML form elements that participate in native constraint validation. Input fields rebuilt entirely in JavaScript without an underlying input element do not support these pseudo-classes.