loke.dev
Header image for The Gentle Timing of :user-valid

The Gentle Timing of :user-valid

Modern CSS finally knows how to stay quiet about form errors until the user actually finishes typing, making your validation logic significantly less aggressive.

· 4 min read

It’s a special kind of digital anxiety when a form field starts screaming "ERROR!" in bright red before you’ve even had a chance to finish your thought. You type the first letter of your email address, and suddenly the UI is judging you for not being finished yet. It’s rude, honestly.

For years, we’ve fought with :invalid. It’s a blunt instrument. It applies the moment a page loads, or the moment a user touches a key, leading to a "user experience" that feels more like an interrogation than a conversation. To fix it, we usually reached for JavaScript, tracking touched or dirty states just to keep the CSS quiet until the right moment.

Then :user-valid and :user-invalid showed up, and they are much more chill.

The problem with being too eager

The standard :invalid pseudo-class doesn't care about your feelings. If a field is required, it is invalid by default the second the DOM renders.

/* The aggressive way */
input:invalid {
  border-color: red;
}

If you use this, your users see a sea of red borders before they’ve even reached for their mouse. It’s aggressive, and it’s why most of us ended up writing 15 lines of boilerplate JS to handle "onBlur" validation logic.

Enter the "User" pseudo-classes

The :user-valid and :user-invalid pseudo-classes behave exactly how we’ve always wanted CSS to behave. They represent an element that is valid (or invalid), but only after the user has significantly interacted with it.

The browser waits for a "commit" of sorts. Usually, this means the user has typed something and then moved focus away from the input (the blur event), or they’ve attempted to submit the form.

Here is how you actually use it in a modern stack:

<form>
  <label for="email">Email address</label>
  <input type="email" id="email" required placeholder="you@example.com" />
  <span class="error-msg">Please enter a valid email.</span>

  <button type="submit">Sign Up</button>
</form>
/* Only shows red when the user is DONE and it's still wrong */
input:user-invalid {
  border: 2px solid crimson;
  background-color: #fff5f5;
}

/* Show a little success nudge when they get it right */
input:user-valid {
  border: 2px solid sea-green;
}

/* Hide error message by default, show it only when invalid */
.error-msg {
  display: none;
  color: crimson;
  font-size: 0.8rem;
}

input:user-invalid + .error-msg {
  display: block;
}

Why this feels better

The difference in "feel" is hard to overstate. With :user-invalid, the validation stays invisible while I'm typing m-y-n-a-m-e-@. The CSS doesn't jump the gun. It waits until I click the next field or hit enter.

It’s the CSS equivalent of a polite waiter who waits for you to finish your sentence before asking if you want dessert, rather than shouting the specials while you're still reading the appetizers.

The "Interacted" Logic

The browser keeps track of whether the user has interacted with the input. Here is the general rule of thumb for when these styles kick in:
1. The user alters the value and moves focus away (blur).
2. The user attempts to submit the form (at which point all fields get the "user" state).
3. If the value was programmatically changed, it usually won't trigger until focus changes.

A Practical Example: The Required Field

We’ve all seen those forms where every empty field is outlined in red the moment you land on the page. It’s messy. By switching to :user-invalid, we can ensure that a required field only looks "wrong" if the user specifically skipped it or tried to submit the form without filling it.

/* 
   This will NOT trigger on page load, 
   even though the field is empty and required. 
*/
input:required:user-invalid {
  outline: 3px solid rgba(255, 0, 0, 0.2);
  border-color: red;
}

What about browser support?

For a long time, this was a "Firefox-only" luxury (under the name :-moz-ui-invalid). But as of late 2023, the green lights are on across the board. Chrome, Safari, and Firefox all support the standard :user-valid and :user-invalid syntax now.

If you have to support ancient browsers, you can technically use :invalid as a fallback, but honestly, the "graceful degradation" here is just... not having validation styles for older browsers. Which, in my opinion, is often better than having *bad* validation styles.

One small "Gotcha"

One thing I noticed while playing with this: :user-valid will trigger if a user types something valid and then tabs away. If you have a green border on :user-valid, your form might end up looking a bit like a Christmas tree once it's all filled out.

I usually prefer to be loud about errors and quiet about success. I’ll often skip :user-valid entirely and only use :user-invalid, unless the form is particularly complex and the user needs that positive reinforcement.

/* Keep it subtle */
input:user-valid {
  border-color: #ccc; /* Don't turn green, just look normal */
}

input:user-invalid {
  border-color: darkred;
}

Wrapping up

It’s rare that a small CSS change can delete a dozen lines of "state management" JavaScript, but :user-valid does exactly that. It brings the browser's native validation UI behavior to our custom CSS styles.

Stop yelling at your users while they’re still typing. Let them finish. Your forms (and your users) will be much calmer for it.