Selector Beginner

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.

Modern
4 lines
1input:user-invalid { border-color: red; }
2input:user-valid { border-color: green; }
3/* no JS, no .touched class */
Old 10 lines
1/* only style after .touched class is added by JS */
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});
type then leave each field to see validation
:user-invalid shows error only after interaction
User valid user invalid
Newly available Since 2023 85% global usage
119+
88+
16.5+
119+

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.

Lines Saved
10 → 4
No JS touched class
Old Approach
.touched + JS blur
Class toggle on every field
Modern Approach
:user-invalid
Browser tracks interaction state

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.

New CSS drops every month.

Get one old → modern comparison in your inbox every week.

ESC