Switching form validation styling from :invalid to :user-invalid alone transforms the experience. The unpleasant behaviour where fields turn red before anyone has typed anything simply goes away. It reached Baseline in October 2023 and works in every modern browser now.
This article narrows in on validating and submitting a form, and how far the platform alone will take you. It covers :user-invalid / :user-valid, setCustomValidity(), showPicker() and requestSubmit(). The attributes on the fields themselves, and how to choose a type, are each handled in a separate article.
Sponsored
The foundations that still carry forms
First, the groundwork. These have worked across browsers for years, and you should check whether they cover your case before writing JavaScript. type="email" and required are not new: they date from HTML5, around 2011.
| Feature | Role | Since |
|---|---|---|
type="email" / url / tel |
Format checking and mobile keyboard switching | HTML5 |
required |
Mandatory input | HTML5 |
pattern |
Format via regular expression | HTML5 |
minlength / maxlength |
Minimum and maximum length | HTML5 |
novalidate |
Disabling built-in validation | HTML5 |
The basic shape:
<form>
<label for="email">Email</label>
<input type="email" id="email" name="email"
autocomplete="email" required>
<button type="submit">Send</button>
</form>
When you specify a format with pattern, add a title alongside it:
<input type="text" name="zip" pattern="\d{3}-?\d{4}"
title="Enter as 123-4567 or 1234567">
Three things worth knowing:
patternis implicitly anchored: you do not write^and$, and you cannot do a partial match- Always pair
patternwithtitle: the title text is used in the error message. Without it users only see a generic complaint - Never use
placeholderas a label: it disappears on first keystroke, taking the field’s meaning with it
The postal code above is a minimal example, cut down to illustrate pattern. A real one also needs inputmode="numeric" and autocomplete="postal-code", which are covered in faster mobile forms with inputmode, autocomplete and enterkeyhint. Choosing between <datalist> and <select>, and the quirks of each type, are in the HTML input element, type by type.
Most importantly, browser-side validation is not a security control. Anyone can add novalidate in DevTools. Server-side validation is always required.
:user-invalid fixes the “red before you type” problem
The flaw in :invalid is that every empty required field is styled as an error from the moment the page loads. You are telling off a user who has done nothing yet.
:user-invalid only matches after the user has interacted with the field and moved on.
/* red from page load */
input:invalid {
border-color: #d33;
}
/* red only after the user has touched it */
input:user-invalid {
border-color: #d33;
}
input:user-valid {
border-color: #2a7;
}
You can drive the error message from CSS as well:
<div class="field">
<label for="mail">Email</label>
<input type="email" id="mail" name="mail" required>
<p class="error">Please enter a valid email address</p>
</div>
.error { display: none; color: #d33; font-size: .875rem; }
input:user-invalid ~ .error { display: block; }
| Selector | Matches when | Use for |
|---|---|---|
:invalid |
The value is invalid, always | Gating the submit button via :has() |
:user-invalid |
Invalid after user interaction | Field appearance |
:user-valid |
Valid after user interaction | Success indicators |
:placeholder-shown |
The placeholder is visible | Floating labels |
Sponsored
Writing error messages in your own words
The browser’s default message (“Please fill out this field”) is often unhelpful. setCustomValidity() replaces it.
const zip = document.getElementById('zip');
zip.addEventListener('input', () => {
// must clear first, or a corrected field stays invalid
zip.setCustomValidity('');
if (zip.validity.valueMissing) {
zip.setCustomValidity('Please enter a postal code');
} else if (zip.validity.patternMismatch) {
zip.setCustomValidity('With or without a hyphen is fine (e.g. 123-4567)');
}
});
Calling setCustomValidity('') at the top is mandatory. Until you pass an empty string, the field stays invalid, so a user who fixes their input still cannot submit. This is a very common bug.
The validity object tells you exactly why a value failed:
| Property | Meaning |
|---|---|
valueMissing |
required but empty |
typeMismatch |
Not a valid email or url |
patternMismatch |
Does not match pattern |
rangeUnderflow / rangeOverflow |
Outside min / max |
tooShort / tooLong |
Violates minlength / maxlength |
stepMismatch |
Not on the step increment |
showPicker() opens date, colour, and suggestion pickers on demand
By default the calendar for <input type="date"> only opens if you hit a small icon precisely. showPicker() lets you open it from anywhere in the field. It has been available in all modern browsers since September 2022.
const dateInput = document.getElementById('date');
dateInput.addEventListener('click', () => {
if (!('showPicker' in HTMLInputElement.prototype)) return;
try {
dateInput.showPicker();
} catch (e) {
// NotAllowedError etc. — fall back to default behaviour
}
});
It works with date, month, week, time, datetime-local, color, file, and inputs with a <datalist>.
Three constraints mean you must wrap it in try:
NotAllowedErrorwithout user activation—you cannot call it from asetTimeoutSecurityErrorin a cross-origin iframe (exceptfileandcolor)InvalidStateErrorif the input isreadonlyordisabled
Sponsored
Auto-submit with requestSubmit(), not this.submit()
Submitting a form when a filter select changes is common, but do not use this.submit().
<!-- validation skipped entirely, submit event never fires -->
<form onchange="this.submit()">
...
</form>
form.submit() bypasses validation and does not fire the submit event, so any pre-submit logic other scripts installed—double-submit prevention, token injection—is silently skipped.
Use requestSubmit(), which behaves exactly as if the submit button had been pressed.
<form id="filter-form" action="/search">
<select name="category">
<option value="all">All</option>
<option value="book">Books</option>
</select>
<noscript><button type="submit">Filter</button></noscript>
</form>
const form = document.getElementById('filter-form');
form.addEventListener('change', () => {
form.requestSubmit(); // validation and submit event both run
});
Related: the submit event exposes which button caused the submission via event.submitter, which is handy for a single form with “Save draft” and “Publish”.
form.addEventListener('submit', (e) => {
if (e.submitter?.value === 'draft') {
// save as draft
}
});
Write submit buttons as <button type="submit"> rather than <input type="submit">, so you can put an icon or several lines of markup inside.
A validation and submission checklist
What I check every time, on the validation and submission side:
- Style errors with
:user-invalid: with:invalideverything turns red the moment the page loads - Pair
patternwithtitle: that text becomes the error message - Clear
setCustomValidity('')before setting a new message, every time - Auto-submit with
requestSubmit():submit()skips both validation and thesubmitevent - Connect
labelandid: do not substituteplaceholderoraria-label(the latter is a last resort when no visible label is possible) - Always validate on the server: browser-side validation is not a security control
The attributes that speed up typing (inputmode, autocomplete, enterkeyhint) are in faster mobile forms with inputmode, autocomplete and enterkeyhint; choosing a type, plus field-sizing and accent-color, in the HTML input element, type by type. Confirmation dialogs are covered in the HTML dialog element, and laying out a form in response to its state in the CSS :has() selector.
Summary
type="email"andrequiredare HTML5 features. Check whether they suffice before reaching for JavaScript- Style errors with
:user-invalid/:user-valid(Baseline October 2023) patternis implicitly anchored, and itstitlebecomes the error message- Always clear
setCustomValidity('')before setting a new message showPicker()opens date, colour, and suggestion pickers on demand. Wrap it intry- Auto-submit with
requestSubmit(), neverthis.submit()—it runs validation and thesubmitevent event.submittertells you which button sent the form- Browser validation is not security
The range of forms you can build without JavaScript has widened considerably over the past few years. Check the platform before reaching for a validation library.