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 everywhere now.
One clarification first: type="email" and required are not 2023 additions. They date from HTML5, around 2011. This article covers those foundations briefly and then spends its length on what genuinely became usable from 2023 onwards: :user-invalid, showPicker(), requestSubmit(), field-sizing, and accent-color.
Sponsored
The foundations that still carry forms
These have worked across browsers for years, and you should check whether they cover your case before writing JavaScript.
| Feature | Role | Since |
|---|---|---|
type="email" / url / tel |
Format checking and mobile keyboard switching | HTML5 |
required |
Mandatory input | HTML5 |
pattern |
Format via regular expression | HTML5 |
<datalist> |
Input suggestions | HTML5 |
<output> |
Displaying a calculated result | HTML5 |
novalidate |
Disabling built-in validation | HTML5 |
<form>
<label for="email">Email</label>
<input type="email" id="email" name="email"
autocomplete="email" required>
<label for="zip">Postal code</label>
<input type="text" id="zip" name="zip"
inputmode="numeric" autocomplete="postal-code"
pattern="\d{3}-?\d{4}"
title="Enter as 123-4567 or 1234567">
<button type="submit">Send</button>
</form>
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
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
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
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
}
});
Sponsored
Auto-resizing fields with field-sizing
Growing a textarea with its content used to require measuring scrollHeight in JavaScript. CSS field-sizing: content reduces it to one line. It reached Baseline in June 2026.
textarea {
field-sizing: content;
min-height: 4lh; /* keep at least four lines */
max-height: 20lh; /* stop it running away */
width: 100%;
}
input[type="text"] {
field-sizing: content;
min-width: 8ch;
}
Setting min-height and max-height together is the trick. Without them the field shrinks to one line when empty and grows to fill the screen with long text. The lh unit lets you express the limits in lines.
Recolouring checkboxes and sliders in one line
Recolouring checkboxes, radios, range, and progress used to mean rebuilding them with appearance: none. accent-color does it in one line.
:root {
accent-color: #2a7fff;
}
You keep the native appearance, keyboard behaviour, and accessibility while swapping in your brand colour. Rebuilding controls with appearance: none purely for looks means reimplementing focus rings and screen reader behaviour yourself—try this first.
Checklist
What I check every time I build a form:
- Connect
labelandid. Do not substituteplaceholderoraria-label(the latter is a last resort when no visible label is possible) - Set
autocompletecorrectly:name,email,tel,postal-code,street-address,one-time-code. The reduction in typing is visible - Match
inputmodeto the keyboard you want:numericversusdecimal - Style errors with
:user-invalid - Auto-submit with
requestSubmit() - Always validate on the server
Input-level attributes are covered in the HTML input element, confirmation dialogs in the HTML dialog element, and state-driven layout in the CSS :has() selector.
Summary
type="email"andrequiredare HTML5 features, not 2023 additions- Style errors with
:user-invalid/:user-valid(Baseline October 2023) showPicker()opens date, colour, and suggestion pickers on demand. Wrap it intry- Auto-submit with
requestSubmit(), neverthis.submit() - Auto-resizing textareas:
field-sizing: content(Baseline June 2026) - Control colours:
accent-color. Do not rebuild withappearance: none - Browser validation is not security
The range of forms you can build without JavaScript has widened considerably. Check the platform before reaching for a validation library.