Form Validation Without JavaScript
The browser already validates forms: required fields, email addresses, patterns, minimum lengths. With :user-invalid CSS shows errors only after a visitor has touched a field — the classic reason for validation scripts is gone.
- JavaScript
- 0 KB
- Built with
required,pattern,:user-invalid,:has()- Browsers
- All current browsers (Chrome, Edge, Firefox, Safari)
- Updated
Yes. Attributes like required, type="email", pattern and minlength make the browser check every field and block the submit with a message. Style the result with :user-invalid and :user-valid, which only apply after the visitor has interacted with a field. Always validate on the server as well.
Live demo
Type into a field and leave it, or press Check the form with empty fields. Nothing turns red until you have interacted with it. The phone field checks mainland Chinese mobile numbers. This demo form never sends data anywhere.
How HTML form validation works
- Attributes define the rules
required,type="email",patternandminlengthtell the browser what a valid value is. On submit, it checks every field, shows a message at the first problem and puts the cursor there. :user-invalidwaits for the visitorThe older
:invalidmatches empty required fields as soon as the page loads — a form full of red before anyone typed.:user-invalidonly matches after the visitor has changed and left a field, or tried to submit.:has()styles the whole field.ui-field:has(:user-invalid)lets the hint below an input turn red — a parent reacting to its child, which used to need a script.- One hint, always connected
Each input points to its hint with
aria-describedby. The same text explains the rule up front and turns into the error message — screen readers always read the right instructions. - A pattern for Chinese mobile numbers
(\+86)?1[3-9][0-9]{9}accepts 11-digit mainland mobile numbers, with or without +86.inputmode="tel"opens the number keypad on phones. - A demo that sends nothing
method="dialog"on a form outside a<dialog>makes the browser validate and then stop. On your site, usemethod="post"and a realaction.
Copy the HTML and CSS
Exactly the code running in the demo. Change method="dialog" to method="post" and add your action URL. All class names start with ui-.
<form class="ui-form" method="dialog">
<div class="ui-field">
<label for="ui-name">Name</label>
<input id="ui-name" name="name" type="text" autocomplete="name" required
aria-describedby="ui-name-hint">
<p class="ui-field__hint" id="ui-name-hint">Required.</p>
</div>
<div class="ui-field">
<label for="ui-email">Email</label>
<input id="ui-email" name="email" type="email" autocomplete="email" required
aria-describedby="ui-email-hint">
<p class="ui-field__hint" id="ui-email-hint">Required. For example name@company.com</p>
</div>
<div class="ui-field">
<label for="ui-phone">Mobile number in mainland China</label>
<input id="ui-phone" name="phone" type="tel" inputmode="tel" autocomplete="tel"
pattern="(\+86)?1[3-9][0-9]{9}" aria-describedby="ui-phone-hint">
<p class="ui-field__hint" id="ui-phone-hint">Optional. 11 digits starting with 1, for example 13812345678 or +8613812345678</p>
</div>
<div class="ui-field">
<label for="ui-message">Message</label>
<textarea id="ui-message" name="message" rows="3" minlength="20" required
aria-describedby="ui-message-hint"></textarea>
<p class="ui-field__hint" id="ui-message-hint">Required, at least 20 characters.</p>
</div>
<button type="submit">Check the form</button>
<p class="ui-form__ok" role="status">All fields are valid. (This demo sends nothing.)</p>
</form>/* Form validation — no JavaScript.
The browser checks required, type, pattern and minlength.
:user-invalid styles a field only after the visitor has
interacted with it — no red fields before anyone has typed. */
.ui-form {
--ui-text: #1b1f3b;
--ui-muted: #5d6280;
--ui-accent: #4f46e5;
--ui-border: #cfd3e6;
--ui-error: #c0262d;
--ui-ok: #0f7a52;
display: grid;
gap: 1rem;
font: 400 0.9375rem/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
color: var(--ui-text);
}
.ui-field {
display: grid;
gap: 0.3rem;
}
.ui-field label {
font-weight: 600;
}
.ui-field input,
.ui-field textarea {
width: 100%;
box-sizing: border-box;
padding: 0.65rem 0.75rem;
border: 1.5px solid var(--ui-border);
border-radius: 8px;
background: #fff;
font: inherit;
color: inherit;
}
.ui-field input:focus-visible,
.ui-field textarea:focus-visible {
outline: 2px solid var(--ui-accent);
outline-offset: 1px;
border-color: var(--ui-accent);
}
.ui-field__hint {
margin: 0;
font-size: 0.8125rem;
color: var(--ui-muted);
}
/* After interaction: red or green */
.ui-field input:user-invalid,
.ui-field textarea:user-invalid {
border-color: var(--ui-error);
}
.ui-field:has(:user-invalid) .ui-field__hint {
color: var(--ui-error);
font-weight: 600;
}
.ui-field:has(:user-invalid) .ui-field__hint::before {
content: "✕ ";
content: "✕ " / "";
}
.ui-field input:user-valid,
.ui-field textarea:user-valid {
border-color: var(--ui-ok);
}
/* Submit button */
.ui-form button {
justify-self: start;
padding: 0.7rem 1.3rem;
border: 0;
border-radius: 8px;
background: var(--ui-accent);
color: #fff;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.ui-form button:focus-visible {
outline: 2px solid var(--ui-accent);
outline-offset: 3px;
}
/* Confirmation, shown only when every field is valid */
.ui-form__ok {
display: none;
margin: 0;
color: var(--ui-ok);
font-weight: 600;
}
.ui-form:valid .ui-form__ok {
display: block;
}Browser validation is for the visitor's convenience. Anyone can switch it off, so your server must check every value again before using it.
Forms for Chinese visitors
Phone number first, email second. In China, people expect to leave a mobile number — many use WeChat more than email. Make the mobile number field fit that: 11 digits, an optional +86, and the number keypad on phones.
Watch out for full-width digits. Chinese input methods can produce full-width characters like 138 instead of 138. They look the same but fail the pattern. Tell visitors which format you expect, and convert full-width characters on the server before you check the value.
Error messages in Chinese. The browser's own validation messages follow the visitor's browser language — a Chinese browser shows Chinese messages. Your hint texts are yours to translate: write them in the page's language.
Accessibility and browser support
| Feature | Without JS | Details |
|---|---|---|
| Validation on submit | All | The browser blocks the submit, shows a message and moves focus to the first problem. |
:user-invalid | All | Supported in all major browsers since 2023. |
| Screen readers | Works | Labels, hints via aria-describedby, and the browser's own error message on submit. |
aria-invalid | Needs JS | Browsers already report invalid fields; setting aria-invalid as well needs a script. |
| Custom error texts per rule | Needs JS | A different message for “empty” and “wrong format” needs setCustomValidity(). One clear hint per field covers most forms. |
| Error summary at the top | Needs JS | For long forms, a script can list all errors after submit. |
:invalid?
Many tutorials — and the first version of this page — style errors with :invalid. It matches every empty required field as soon as the page loads, so visitors see a red form before typing anything. :user-invalid fixes exactly that.
Forms, speed and SEO
Search engines don't rank forms. You may read that validation attributes help crawlers classify a page as a “lead page”. There is no evidence for that, and we wouldn't rely on it. What matters is the content around the form.
But speed and reliability matter. A validation library adds JavaScript that has to download and run before the form works properly — on slow mobile networks in China that can take a while. Native validation works from the first byte, and it keeps working when a script fails to load. For landing pages from Baidu Ads, every lost form submission is money spent.
Questions about form validation without JavaScript
Can HTML validate a form without JavaScript?
Yes. required, type, pattern, min, max, minlength and maxlength are checked by the browser before the form is sent.
What is the difference between :invalid and :user-invalid?
:invalid applies immediately, even to untouched empty fields. :user-invalid applies only after the visitor has interacted with the field or tried to submit.
What pattern validates a Chinese mobile number?
(\+86)?1[3-9][0-9]{9} accepts mainland mobile numbers like 13812345678, with an optional +86. Our phone number validator builds patterns for landlines and other formats too.
Do I still need server-side validation?
Always. Browser validation is a convenience for visitors. Anyone can send data to your server without the form.
Can I show my own error messages without JavaScript?
Yes, as hint text below each field that turns red with :user-invalid, as in the demo. Changing the browser's own pop-up message needs a script.
This FAQ is built without JavaScript — see the accordion pattern.
Are your landing page forms losing leads in China?
We build and test Chinese landing pages for Baidu Ads and paid social: forms that accept Chinese phone numbers, load fast in China and work on every phone. Then we run the campaigns for Western companies.
CSS-First UI Patterns are written and maintained by Jademond Digital, a Chinese digital marketing agency founded in Guangzhou in 2012. We build and audit websites for Baidu, Google and AI search — which is why we care what works without JavaScript. About us →