Form validation styles without JavaScript
:invalid fires as soon as the page loads, marking empty required fields as errors before anyone types. The workaround was JS adding a .touched class after blur. :user-invalid only activates after the user has interacted with the field.
2input:user-valid { border-color: green; }
3/* no JS, no .touched class */
2input.touched:invalid { border-color: red; }
3input.touched:valid { border-color: green; }
4// JS: add .touched class on blur
5input.addEventListener('blur', () => {
6 input.classList.add('touched');
7});
Since 2023 this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.
No blur listener
No JavaScript event listener, no .touched class, no class toggling on every field.
Interaction-aware
:user-invalid only triggers after the user has interacted with the field. Empty required fields stay neutral on page load.
Pairs with :user-valid
:user-valid shows success state the same way. Both follow the same interaction threshold as :user-invalid.
How it works
:invalid applies the moment the page loads. A required empty field is immediately styled as an error before the user touches it. The fix was JavaScript: listen for blur on each input, add a .touched class, then use .touched:invalid in CSS to defer the error styling until after first interaction.
:user-invalid and :user-valid are built-in pseudo-classes that match after the user has interacted with a field. :user-invalid activates when the field is left in an invalid state. :user-valid activates on a valid state. The browser tracks the interaction threshold — no JavaScript, no class management.