Your Required Field Turns Red Before You Type Anything
A signup form's email input shows a red border the instant the page loads — before the visitor has typed a single character. The obvious CSS causes it, and the obvious fix is a native pseudo-class nobody reaches for first.

Open a signup form, before you've clicked into a single field. The email input already has a red outline. You haven't typed anything. You haven't done anything wrong. The page just loaded, and it's already telling you that you failed.
Somebody wrote this:
Reasonable-looking rule. Ships in every framework's forms tutorial. And it's the entire reason the field is red before the visitor exists to the page for more than a few milliseconds.
:invalid isn't about whether the user made a mistake. It's about whether the current value fails constraint validation, full stop — checked continuously, starting from paint. An empty <input required> fails the "has a value" constraint the moment it renders, so :invalid matches immediately, no interaction required. Same for type="email" with anything that isn't a valid address, pattern mismatches, minlength shortfalls — all of it evaluated from the first frame.
CSS is being completely honest here: the value genuinely doesn't satisfy the constraints. The bug isn't in the selector. It's in using "is this value currently invalid" as a proxy for "should I show the user an error," when those are different questions until the user has actually tried.
Runs right in your browser — poke at it and watch the concept react live.
The instinct isn't to remove validation styling — the red border is genuinely useful once the user has had a shot at the field. It's to gate it on interaction. So teams build a "touched" flag:
This works. It's also why form libraries like Formik and React Hook Form ship a whole touched object mirroring every field, why every hand-rolled form has a scattering of onBlur handlers whose only job is flipping a boolean, and why that boolean has to be threaded through every input component in the tree. None of it is validating anything — the browser was already doing that part correctly. It's just gating when to show it, in JavaScript, because CSS supposedly couldn't ask "has this user interacted with the field yet."
It can. It has been able to since 2021.
Same styling, no touched state, no onBlur, no boolean threaded through props. :user-invalid matches a field that fails validation and that the user has interacted with; :user-valid is the mirror, for a field that passes. "Interacted with" means one of two things happened: the field committed an edit — you typed and then left it, the same moment that would fire a native change event — or the form had a submission attempted. Before either of those, neither pseudo-class matches, so the field sits neutral on page load exactly the way it should.
It shipped in Firefox first (Firefox 88, April 2021), then Safari (16.4, March 2023), then Chrome and Edge last (Chrome 119, November 2023) — so it's been in every evergreen browser for a while now, no polyfill, no library.
Type into that email field and leave it — tab away, or click elsewhere — and :user-invalid lights up if it's still malformed. That first blur is what unlocks it. Here's the part that's easy to miss: once it's unlocked, it keeps tracking live. Click back into the field and keep editing without blurring again, and the moment the value becomes a valid address, it flips straight to :user-valid — no second blur required. It only needed that one commit to start paying attention; after that, it's watching every keystroke, which is exactly what a hand-rolled touched flag usually fails to do, since most implementations set touched once on blur and then never revisit it.
Think it clicked? Take the 7-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
Try this in the playground above: get a field into its red :user-invalid state, then click the form's reset button. The naive expectation is that resetting clears the value back to empty — and an empty required field is still invalid, so surely it should still be red?
It isn't. :user-invalid and :user-valid both stop matching the moment the form resets, because resetting the form clears the browser's internal "has this user interacted with this control" flag along with the value. The field goes back to a clean, neutral slate — not because the value became valid, but because the interaction that earned the styling got wiped too. A touched-flag implementation has to remember to do this by hand (reset the boolean in the same handler that resets the form state); the native pseudo-classes do it automatically, because resetting is specified to undo exactly the thing that made them match in the first place.
It's a small detail, and it's exactly the kind of thing that's easy to get right by accident with a library's abstraction and easy to get wrong by hand — which is a good sign you're better off leaning on the platform than re-implementing its bookkeeping.
Go find the :invalid { border-color: red; } line in your own forms, or the touched object doing the same job through a chain of onBlur handlers, and try swapping in :user-invalid / :user-valid instead. What's the ugliest "red on page load" form you've had to fix?
🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 💬 Discord — join the frontend best-practices community: discord.gg/d9KRhuAwQ
- 📸 Instagram — frontend best practices, daily: @bestpractice___
Keep reading
One post a day, in your inbox
Each one with a runnable playground and a quiz. No pitch, no digest, unsubscribe in one click.
0 comments
Sign in to join the discussion, like comments, and save articles for later.