HTML gives you built-in form validation without any JavaScript: pick the right type on each <input> (like email, url, number or date) and add attributes such as required, minlength, maxlength, min, max, step and pattern. The browser then blocks submission and shows a message until the values fit. In this guide I go through the input types and validation attributes I use, with examples I checked in a current Chrome build, plus the gotchas that caught me out.
One thing to say right away: browser validation is there to help the user. It's not security. Anyone can turn it off or send a request without your form, so the server has to check everything again.
A sign-up form using HTML validation only
Here's a small, complete form. There's no JavaScript, but it already refuses empty names, short passwords, badly formed emails and ages outside the range:
<form action="/signup" method="post">
<label for="name">Full name</label>
<input id="name" name="name" type="text" autocomplete="name" required minlength="2" maxlength="60">
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email" required>
<label for="password">Password (at least 8 characters)</label>
<input id="password" name="password" type="password" autocomplete="new-password" required minlength="8">
<label for="age">Age</label>
<input id="age" name="age" type="number" min="13" max="120" step="1">
<button type="submit">Create account</button>
</form>
A few things are doing quiet work here:
-
Every input has a
<label>linked withforandid. Clicking the label focuses the field, and screen readers read it out. A placeholder is not a replacement for a label, because it disappears as soon as you type. -
nameis what the server receives. An input without anameisn't sent at all. -
autocompletevalues likeemail,nameandnew-passwordlet browsers and password managers fill the form correctly.
HTML input types and what they give you
Choosing the right type gives you three things: a validation rule, a better keyboard on phones and sometimes a native picker.
<label for="site">Website</label>
<input id="site" name="site" type="url" placeholder="https://example.com">
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel" autocomplete="tel">
<label for="exam">Exam date</label>
<input id="exam" name="exam" type="date" min="2026-01-01" max="2026-12-31">
<label for="start">Start time</label>
<input id="start" name="start" type="time" step="900">
<label for="hours">Study hours per day</label>
<input id="hours" name="hours" type="range" min="0" max="12" step="0.5" value="3">
<label for="accent">Accent colour</label>
<input id="accent" name="accent" type="color" value="#8b7cff">
<label for="q">Search notes</label>
<input id="q" name="q" type="search">
-
emailchecks the basic shape of an address. In my testing,abcwas rejected with "Please include an '@'", buta@bwas accepted, because the spec allows addresses without a dot (like on an intranet). -
urlneeds a scheme.example.comfails;https://example.compasses. -
teldoesn't validate anything by itself, because phone formats vary so much between countries. It mainly brings up a number pad on phones. Add apatternif you need a format. -
numberworks withmin,maxandstep. Withstep="1", a value of 13.5 fails with a step error. -
dateandtimeshow native pickers. The value is always sent in a fixed format (2026-11-15for dates,14:30for times), no matter how it's displayed to the user. -
rangealways has a value, and it clamps tomin/max. When I set it to 13 withmax="12", the value became 12. -
coloropens a colour picker and sends a hex value like#8b7cff. -
searchbehaves like text but may show a clear button and fits nicely inside a<search>element.
The validation attributes
required
The field must have a value. For a group of radio buttons with the same name, putting required on one of them makes the whole group required. On a checkbox, it means "must be ticked", which is what you want for "I agree to the terms".
minlength and maxlength
Limits on the number of characters for text-like inputs. maxlength stops the user from typing more. minlength is checked when the user has edited the field. One thing I found while testing: setting a too-short value from JavaScript didn't count as invalid, because the browser only applies minlength to values the user typed.
min, max and step
For number, range and the date and time types. Dates use the same format as the value: min="2026-01-01". For time, step is in seconds, so step="900" means 15-minute steps.
pattern
A regular expression the whole value must match. You don't need ^ and $, because the pattern is always matched against the entire value.
<label for="username">Username</label>
<input id="username" name="username" type="text"
required pattern="[a-z0-9_]{3,16}"
title="3 to 16 characters: lowercase letters, numbers and underscores">
<label for="mobile">Mobile number (11 digits, starting with 01)</label>
<input id="mobile" name="mobile" type="tel" inputmode="numeric"
pattern="01[0-9]{9}" autocomplete="tel">
The title text is shown as a hint alongside the browser's "Please match the requested format" message in many browsers, so use it to explain the rule in plain words. In my test, mahir_01 passed, while Ab and abc! failed. The mobile pattern accepted 01712345678 and rejected 0171.
Radio buttons, checkboxes and suggestions
<fieldset>
<legend>Preferred language</legend>
<input id="lang-py" type="radio" name="lang" value="python" required>
<label for="lang-py">Python</label>
<input id="lang-js" type="radio" name="lang" value="javascript">
<label for="lang-js">JavaScript</label>
</fieldset>
<label for="city">City</label>
<input id="city" name="city" type="text" list="cities">
<datalist id="cities">
<option value="Dhaka"></option>
<option value="Chattogram"></option>
<option value="Sylhet"></option>
</datalist>
<input id="terms" name="terms" type="checkbox" required>
<label for="terms">I agree to the terms</label>
<fieldset> and <legend> group related radio buttons and give the group a name that screen readers announce. <datalist> suggests values but still lets the user type anything. If only those values are allowed, use a <select> instead.
Styling valid and invalid fields
The old :invalid selector matches as soon as the page loads, so an empty required field shows up red before the user has done anything. The newer :user-invalid and :user-valid pseudo-classes only match after the user has interacted with the field, and they're supported in current major browsers:
input:user-invalid {
border-color: #d93025;
outline-color: #d93025;
}
input:user-valid {
border-color: #188038;
}
Custom rules with a little JavaScript
Some rules can't be written as attributes, like "both passwords must match". The Constraint Validation API lets you plug your own rule into the same system with setCustomValidity():
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Confirm password</title>
</head>
<body>
<form id="signup">
<label for="pw">Password</label>
<input id="pw" name="pw" type="password" required minlength="8">
<label for="pw2">Confirm password</label>
<input id="pw2" name="pw2" type="password" required>
<button type="submit">Sign up</button>
</form>
<script>
const pw = document.getElementById("pw");
const pw2 = document.getElementById("pw2");
function checkMatch() {
if (pw2.value !== pw.value) {
pw2.setCustomValidity("Passwords do not match.");
} else {
pw2.setCustomValidity(""); // empty string = valid again
}
}
pw.addEventListener("input", checkMatch);
pw2.addEventListener("input", checkMatch);
</script>
</body>
</html>
A non-empty message marks the field invalid and blocks submission with your text. An empty string clears the error. I loaded this page in headless Chrome and set the values from a script: with different passwords, validity.customError was true, and once they matched the field was valid again.
Common mistakes with HTML form validation
1. Trusting it on the server
Client-side validation can be skipped with dev tools or a direct request. Always validate again on the server. In my Quizwright project, the server route checks every request against a strict schema before it does anything, which I explained in how I kept my AI API key off the browser.
2. Unescaped hyphens in pattern
Modern browsers compile pattern with the JavaScript v flag, which is stricter about character classes. In my test, pattern="[a-z0-9-]{3,}" was silently ignored, so even AB passed. Writing it as [a-z0-9\-]{3,} made the pattern work again. If a pattern seems to do nothing, check the console for a regex error.
3. Using placeholder as the label
Placeholders vanish while typing and often have low contrast. Keep a visible <label>.
4. Using type="number" for things that aren't numbers
Phone numbers, OTP codes and card numbers aren't quantities. number adds spinner arrows, accepts input like 1e5, and once the value is treated as a number, leading zeros are easy to lose. Use type="text" or tel with inputmode="numeric" and a pattern.
5. Forgetting the name attribute
The field validates perfectly and then never reaches the server.
FAQ
How do I turn off browser validation?
Add novalidate to the <form> to skip it for the whole form, or formnovalidate to a specific submit button, for example a "Save draft" button.
Can I change the default error message?
Yes, with setCustomValidity() in JavaScript, as in the password example. The title attribute adds a hint for pattern errors but doesn't replace the browser's message.
Is the email type enough to validate emails?
It catches typos like a missing @, but it accepts addresses like a@b. On the server, check the format again, and if the address really matters, send a confirmation email.
Do these attributes work on textarea and select?
required works on both, and minlength and maxlength work on <textarea>.
Conclusion
Start every form with the right input types and labels, then add required, length limits, min/max/step and pattern where they fit. Style errors with :user-invalid, use setCustomValidity() for rules that need code, and always validate again on the server.
MDN's guide to HTML form validation and the Constraint Validation API covers every attribute in detail and is worth bookmarking.
Originally published at mfaysal.com.
Top comments (0)