A deep dive into :invalid, :user-invalid

When we talk about web accessibility (a11y), we usually think of ARIA attributes, screen readers, or keyboard navigation first.
But in real user experiences, the most frequently encountered accessibility challenge is forms.
“Is this email format correct?”
“This field is required.”
“Your password is too short.”
At the core of this experience are two CSS pseudo-classes:
:invalid:user-invalid
In this article, we’ll break down what they mean, how they differ, and how to use them correctly.
✨ 1. :invalid — The browser’s validation state
:invalid represents the browser’s native constraint validation state for form controls. It is automatically applied based on HTML5 form validation rules.
For example:
<input type="email" required />
As soon as the page loads, this input is considered :invalid, because it violates the required constraint.
Important takeaway
:invaliddoes not mean “the user made a mistake.”
It simply means “the current value does not satisfy the validation rules.”
Because of this, styling directly with :invalid often causes poor UX:
Red borders before the user types anything
Error messages shown before any interaction
✨ 2. :user-invalid — Invalid after user interaction
To address this problem, CSS introduced :user-invalid (Selectors Level 4).

The key idea is simple:
:user-invalidonly applies after the user has interacted with the form control.
You might now say, “Now it makes sense!”
❌ 3. Don’t mix :invalid and :user-invalid together
Let’s say you do this:
Form A uses
:invalidForm B uses
:user-invalidDifferent forms
Different wrappers
Different CSS scopes
You expect them to behave independently.
But they don’t.
As soon as any :invalid selector exists in the document,:user-invalid starts behaving like plain :invalid.
Errors appear before user input.
This happens even if:
the forms are separate
the selectors are scoped
the DOM structure is clean
🤔 4. Why this happens (the real reason)
:user-invalid is not an independent state
It depends on :invalid.
Internally, browsers must compute the :invalid state first in order to determine whether :user-invalid applies.
It’s also important to understand that CSS selectors are evaluated at the document level, not at the form level. CSS has no notion of “scope” in this context — selectors are always matched against the entire document tree.
Once the browser encounters any selector that references :invalid, it switches the document into a constraint-validation-aware state. As part of this optimization, the browser eagerly computes validation states for form controls.
During this process, the “user interaction delay” condition that :user-invalid is meant to provide can be effectively bypassed. This behavior is not a specification violation, but rather an intentional implementation trade-off made by browser engines to balance correctness and performance.
Next time,
we’ll dive into the ARIA attributes used in forms.
Let me know in the comments if there are any attributes you’ve been curious about! 😉


