Built-in validation
required, pattern, min, max, minlength, maxlength — and the :invalid and :valid pseudos that style the result.
The browser already validates your form for you, before it submits. The rules are attributes you sprinkle on inputs. Required, length limits, value ranges, regex patterns — all of them ship in the box. None of them need JavaScript.
The catch: the default error UI is plain. A bubble appears next to the field. It is in the user's browser language. It is not very pretty. This lesson covers the rules themselves and the CSS pseudos that let you style the result.
The validation attributes
A short tour. Each attribute is one word on an input.
required— the field cannot be empty (or unchecked, for checkboxes).minlength="N"— minimum number of characters in the value.maxlength="N"— maximum number of characters; the browser refuses keystrokes past the limit.min="N"— minimum value (numbers, dates, ranges).max="N"— maximum value.step="N"— value must be a multiple of N from the min (numbers, dates, ranges).pattern="regex"— value must match the regex. (Text-shaped types only.)type="email",type="url"— the type itself adds validation.
<form action="/signup" method="post">
<label>
Username
<input name="username" type="text"
required minlength="3" maxlength="20"
pattern="[a-z0-9_]+">
</label>
<label>
Email
<input name="email" type="email" required>
</label>
<label>
Age
<input name="age" type="number" required min="13" max="120" step="1">
</label>
<button type="submit">Sign up</button>
</form>Submit empty: each required field gets a "please fill out this field" message. Type "ab" in username: "please lengthen this text to 3 characters or more". Type "FOO!": "please match the requested format" (the pattern allows only lowercase letters, digits, underscores). Type "5" in age: "value must be greater than or equal to 13".
The browser also focuses the first invalid field for you and scrolls to it. No useEffect needed.
A pattern primer: [a-z0-9_]+ means "one or more characters, each a lowercase letter, digit, or underscore". The whole value must match — the pattern is implicitly anchored to the start and end. Patterns are full JavaScript regex; everything from a single class up to a complex group expression works.
<!-- a US ZIP code: 5 digits, optionally a dash and 4 more -->
<input name="zip" pattern="[0-9]{5}(-[0-9]{4})?" required>
<!-- A simple username: 3-20 lowercase letters, numbers, underscore, hyphen -->
<input name="username" pattern="[a-z0-9_-]{3,20}" required>The title attribute on a patterned input is shown as part of the error message — set it to a human description of what is wrong:
<input
name="username"
pattern="[a-z0-9_]{3,20}"
required
title="3 to 20 lowercase letters, numbers or underscores">The browser's bubble then says "please match the requested format" plus the title. Useful when the pattern is not self-explanatory.
The browser only enforces these rules on form submit, not as the user types. The error bubble appears after the user clicks Submit. If you want feedback as they type, wire up :invalid styling and watch for the input event yourself.
<input type="email" required> on the email field. The user types "abc" and clicks Submit. What does the browser do?:invalid and :valid
Two CSS pseudo-classes track the validation state of an input live, as the user types.
:valid— the input currently passes all its validation rules.:invalid— the input currently fails at least one rule.
input:invalid {
border-color: red;
}
input:valid {
border-color: green;
}Style the borders, the background, an icon — anything CSS can target. The user gets visual feedback the second the rule passes or fails.
But there is a problem. The pseudo applies immediately, even before the user has typed a single character. An empty required field is :invalid from the moment the page loads. Style it red and your form looks broken before the user has done anything.
<!-- This input lights up red the instant the page renders. --> <input type="email" required>
A common workaround used to be :invalid:not(:placeholder-shown) — hide the red until the user types something. It works, but it is a hack on top of a hack. Modern browsers ship a cleaner pseudo built for this exact case.
:user-invalid, the better one
:user-invalid is :invalid but only after the user has interacted with the field — typed in it, blurred it, or tried to submit. Until then, the input is neither :user-valid nor :user-invalid.
input:user-invalid {
border-color: red;
background: #fee;
}
input:user-valid {
border-color: green;
}The user lands on the page; the email field is plain. They tab past it without typing — the field is now :user-invalid (touched and required, with no value), so it lights up red. They go back, type a valid email — the field flips to :user-valid and turns green.
This is the default you want. It mirrors how every well-designed form library validates: be quiet until the user has interacted, then show the result.
A useful picture: :invalid is the engine; :user-invalid is the engine with the "don't yell at someone who hasn't touched anything yet" wrapper.
:user-invalid is supported in every recent evergreen browser. For older targets, fall back to :invalid:not(:focus):not(:placeholder-shown) as the last-mile workaround. The intent is the same: only show the error after the user has had a chance to fill it in.
input:invalid { border-color: red } to your CSS. A user lands on a fresh signup page with three required fields. What do they see before typing anything?Turning it off
Sometimes you want to validate the form yourself in JavaScript and skip the browser's bubbles entirely. Two escape hatches.
novalidate on the form — the browser stops blocking submission. The validation pseudos (:valid, :invalid) still apply, so your CSS keeps working. The form just submits no matter what.
<form action="/signup" method="post" novalidate> ... </form>
formnovalidate on a single button — only that button skips validation when it submits. Useful for "Save draft" buttons that should not require all fields to be filled.
<form action="/article" method="post"> <input name="title" required> <textarea name="body" required></textarea> <button type="submit">Publish</button> <button type="submit" formaction="/article/draft" formnovalidate>Save draft</button> </form>
Two submit buttons in one form. The "Publish" button uses the form's action and runs full validation. The "Save draft" button overrides both — submits to /article/draft and skips validation, so an unfinished article saves cleanly.
required on the title. What's the smallest fix?<input type="number" min="13" max="120" step="0.5"> for an age field. The user types "13.7". What happens on submit?