Update 2023-09-22: Nowadays, browsers support the :user-invalid
/:user-valid
pseudo-class selectors that solve this exact problem. See the update for all details.
Update 2021-01-28: In case your form controls have the required
attribute set, you’ll even want to use the more extensive :not(:focus):not(:placeholder-shown):invalid
selector. See the update for all details.
We’ve all been in this situation, where the built-in form validation of the browser starts complaining while you’re still entering data into the form:
I prefer when forms wait for blur before freaking out 🤨
— Ryan Florence (@ryanflorence) January 27, 2021
Highly annoying, but thankfully there’s an easy way to fix this.
~
# The problem
The problem is caused by a piece of CSS similar to this snippet:
.error-message {
display: none;
}
input:invalid {
border-color: var(--color-invalid);
}
input:invalid ~ .error-message {
display: block;
}
input:valid {
border-color: var(--color-valid);
}
When entering an e-mail address — as Ryan is doing above — this is extremely annoying as your e-mail address is only valid when you’re done entering it. Try it in the demo below.
See the Pen
Form Validation on Blur (1/4) by Bramus (@bramus)
on CodePen.
Ugh! 🤬
💁♂️ For a slight moment you’ll notice that an e-mail address in the form of bramus@bram
(e.g. without a .tld
suffix) is also considered valid. As per RFC 822 the user@hostname
format — used mainly in local networks — indeed is allowed.
~
# Making things better
It would be nice to only perform the validation when the field is not being edited anymore. In CSS we don’t have a blur event, but what we do have is a pseudo-class selector to indicate whether an input has the focus: :focus
. Combine that with :not()
and we have a way to target the “not being focussed” state, which also indicates that the field is not being edited.
Putting it all together, our CSS becomes this:
.error-message {
display: none;
}
input:not(:focus):invalid {
border-color: var(--color-invalid);
}
input:not(:focus):invalid ~ .error-message {
display: block;
}
input:not(:focus):valid {
border-color: var(--color-valid);
}
This way the validations only happen when you’re blurred out of the form.
See the Pen
Form Validation on Blur (2/4) by Bramus (@bramus)
on CodePen.
Ah, that’s better! 😊
~
# Making things even more better
In the demo above you’ll see one small side-effect though: the border is green by default, even though we didn’t enter any value. This is not exactly what we want Ideally we only want to validate in case the field is both not focussed and not empty.
In CSS we can’t use :empty
for this though, as :empty
targets elements that have no children/innerHTML
content. What we can do however is abuse the :placeholder-shown
pseudo-class.
- If there’s no text entered, the placeholder is shown
- If there is text entered, the placeholder is not shown
With this in mind, our code now becomes this:
.error-message {
display: none;
}
input:not(:focus):invalid {
border-color: var(--color-invalid);
}
input:not(:focus):invalid ~ .error-message {
display: block;
}
input:not(:focus):not(:placeholder-shown):valid {
border-color: var(--color-valid);
}
⚠️ Do note that this requires a value for the input
‘s placeholder
.
<input type="email" placeholder="you@example.org" />
If you don’t want any placeholder to show, set its value to
(space)
Here’s an adjusted demo:
See the Pen
Form Validation on Blur (3/4) by Bramus (@bramus)
on CodePen.
Yes, exactly what we want! 🤩
:user-invalid
pseudo class for exactly this use-case.
The
:user-invalid
pseudo-class represents an element with incorrect input, but only after the user has significantly interacted with it.
This feature is still in the works and not supported yet. Firefox supports it using the non-standard ::-moz-ui-invalid
name. Thanks for the tip, Schepp!
~
# Update 2021-01-28: Playing nice with required
As reader Corey pointed out in the comments below the code above does not play nice with the required
attribute. When the attribute is added, the error message will be shown when the form loads.
To work around this we also need to include the :not(:placeholder-shown)
pseudo-class in our :invalid
selectors.
.error-message {
display: none;
}
input:not(:focus):not(:placeholder-shown):invalid {
border-color: var(--color-invalid);
}
input:not(:focus):not(:placeholder-shown):invalid ~ .error-message {
display: block;
}
input:not(:focus):not(:placeholder-shown):valid {
border-color: var(--color-valid);
}
Putting it all together, here’s an updated demo:
See the Pen
Form Validation on Blur (4/4) by Bramus (@bramus)
on CodePen.
Phew! 😅
~
# Update 2023-09-22: Using :user-invalid
As mentioned earlier in this article, the :user-invalid
pseudo-class solves this very issue. As per MDN:
The
:user-invalid
CSS pseudo-class represents any validated form element whose value isn’t valid based on their validation constraints, after the user has interacted with it.
Back when this article was originally published, no browser supported this pseudo-class. By now, all browsers have caught up and come with support for the :user-invalid
pseudo.
- Chromium (Blink)
-
✅ Supported as of Chromium 119.0.5999.0. Chrome 119 will hit a stable release in October 2023.
- Firefox (Gecko)
-
✅ Supported as of Firefox 88.
Earlier versions of Firefox, starting at version 4, supported the
:-moz-ui-invalid
alias. - Safari (WebKit)
-
✅ Supported as of Safari 16.5
Putting it all together, here’s the final demo:
See the Pen
Form Validation on Blur (5/4) by Bramus (@bramus)
on CodePen.
~
🔥 Like what you see? Want to stay in the loop? Here's how:
Note: The title of this post is definitely a reference to this post on CSS-Tricks and this post by Kilian.
Note that if you add `required` to the input then it will still always show the error style on page load as well.
Oh, nice catch!
To work around that, one also needs to add `:not(:placeholder-shown)` to the `:invalid` selectors. I’ll update post accordingly, thanks!
There’s one thing missing still. I, personally, want to see the error message while I type AFTER initial validation was done.
1. I type “d”
2. press “TAB”, error shown
3. I click at input field, error disappears (this is not good)
4. I type correct email and don’t see if error is gone until I hit “TAB” (also bad)
I like react-final-form approach with “touched” status:
`const showError = meta.touched && !!meta.error`. “touched” is set on blur, so after user touched field he’ll see the validation errors for this field. I feel, this gives better UX
There still is an issue with that approach on mobile and touch devices, or even with keyboard use (haven’t try that myself, but I assume).
When the input is focused, and the user clicks Enter while the input is focused (to submit the form) – then the error won’t be displayed…
This also happens on desktop, if you press enter to submit the form.
This use-case is tackled by the `:user-invalid` pseudo-class mentioned. Unfortunately it’s only supported in Firefox (using a prefix).
Is there a similar solution for date fields?