:user-invalid is the validation selector you actually wanted
Unlike :invalid, it waits until the user has interacted with the field — so empty required inputs stop turning red before anyone has typed anything.
Every form built with :invalid has the same bug: the fields are red on page load, because an empty required input is genuinely invalid and the browser is being honest about it.
The usual fix is a JavaScript “touched” flag copied from a framework. The platform has had the answer for a while:
input:user-invalid {
border-color: var(--danger);
}
input:user-invalid + .hint::before {
content: '⚠ ';
}
:user-invalid matches only after the user has edited the field and moved on, or after a submit attempt. Exactly the behaviour every form library reimplements.
Why it matters: combined with :has(), this removes client-side validation state from JavaScript entirely for most forms — the field, its label and its hint can all restyle from one selector on the wrapper. Server-side validation still does the real work; this is just about not shouting at people before they have started.
Baseline widely available across engines.