Basic Form Validation with JavaScript Tutorial
Learn JavaScript form validation from scratch. Master required fields, email patterns, password rules, real-time feedback, the Constraint Validation API, and custom error messages.
Form validation ensures users submit correct data before it reaches your server. While server-side validation is always required, client-side validation with JavaScript gives instant feedback, reduces unnecessary network requests, and creates a better user experience. This tutorial covers everything from simple required-field checks to the built-in Constraint Validation API.
Why Validate on the Client Side?
| Benefit | Description |
|---|---|
| Instant feedback | Users see errors immediately without waiting for a server response |
| Reduced server load | Invalid submissions never reach the server |
| Better UX | Highlights exactly which field needs fixing |
| Faster corrections | Users fix errors without losing other form data |
Client-side validation is a convenience feature, not a security feature. Always validate on the server too, because JavaScript can be disabled or bypassed.
Simple Validation with JavaScript
Required Field Check
function validateRequired(input) {
const value = input.value.trim();
if (value === "") {
showError(input, "This field is required");
return false;
}
clearError(input);
return true;
}
function showError(input, message) {
input.classList.add("invalid");
input.classList.remove("valid");
const errorEl = input.parentElement.querySelector(".error-message");
if (errorEl) errorEl.textContent = message;
}
function clearError(input) {
input.classList.remove("invalid");
input.classList.add("valid");
const errorEl = input.parentElement.querySelector(".error-message");
if (errorEl) errorEl.textContent = "";
}Email Validation
function validateEmail(input) {
const email = input.value.trim();
if (email === "") {
showError(input, "Email is required");
return false;
}
// Basic email pattern check
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(email)) {
showError(input, "Please enter a valid email address");
return false;
}
clearError(input);
return true;
}Password Validation
function validatePassword(input) {
const password = input.value;
const errors = [];
if (password.length < 8) {
errors.push("At least 8 characters");
}
if (!/[A-Z]/.test(password)) {
errors.push("At least one uppercase letter");
}
if (!/[a-z]/.test(password)) {
errors.push("At least one lowercase letter");
}
if (!/[0-9]/.test(password)) {
errors.push("At least one number");
}
if (errors.length > 0) {
showError(input, errors.join(", "));
return false;
}
clearError(input);
return true;
}Confirm Password Match
function validatePasswordMatch(passwordInput, confirmInput) {
if (confirmInput.value === "") {
showError(confirmInput, "Please confirm your password");
return false;
}
if (passwordInput.value !== confirmInput.value) {
showError(confirmInput, "Passwords do not match");
return false;
}
clearError(confirmInput);
return true;
}Putting It All Together
const form = document.getElementById("signup-form");
const nameInput = document.getElementById("name");
const emailInput = document.getElementById("email");
const passwordInput = document.getElementById("password");
const confirmInput = document.getElementById("confirm-password");
form.addEventListener("submit", (e) => {
e.preventDefault();
const isNameValid = validateRequired(nameInput);
const isEmailValid = validateEmail(emailInput);
const isPasswordValid = validatePassword(passwordInput);
const isConfirmValid = validatePasswordMatch(passwordInput, confirmInput);
if (isNameValid && isEmailValid && isPasswordValid && isConfirmValid) {
console.log("Form is valid! Submitting...");
form.submit(); // Or use fetch for async submission
}
});The Constraint Validation API
Modern browsers have a built-in Constraint Validation API that works with HTML validation attributes like required, minlength, pattern, and type:
Validity State Properties
const input = document.getElementById("email");
// The validity object has boolean properties
console.log(input.validity.valid); // true if all constraints pass
console.log(input.validity.valueMissing); // true if required and empty
console.log(input.validity.typeMismatch); // true if type="email" but invalid format
console.log(input.validity.tooShort); // true if shorter than minlength
console.log(input.validity.tooLong); // true if longer than maxlength
console.log(input.validity.patternMismatch); // true if fails pattern regex
console.log(input.validity.rangeUnderflow); // true if below min
console.log(input.validity.rangeOverflow); // true if above max
console.log(input.validity.stepMismatch); // true if doesn't match step
console.log(input.validity.customError); // true if setCustomValidity was calledValidity Properties Reference
| Property | Trigger | HTML Attribute |
|---|---|---|
valueMissing | Field is empty | required |
typeMismatch | Wrong format | type="email" or type="url" |
tooShort | Below minimum length | minlength="8" |
tooLong | Above maximum length | maxlength="50" |
patternMismatch | Fails regex pattern | pattern="[A-Za-z]+" |
rangeUnderflow | Number below minimum | min="1" |
rangeOverflow | Number above maximum | max="100" |
stepMismatch | Number not matching step | step="5" |
customError | Custom message set | setCustomValidity() |
checkValidity and reportValidity
const form = document.getElementById("my-form");
const emailInput = document.getElementById("email");
// checkValidity: returns boolean, triggers 'invalid' event if false
console.log(emailInput.checkValidity()); // true or false
// reportValidity: returns boolean AND shows browser tooltip
console.log(emailInput.reportValidity()); // Shows error popup
// Check entire form at once
console.log(form.checkValidity()); // true only if ALL inputs are valid
// Listen for the invalid event
emailInput.addEventListener("invalid", (e) => {
e.preventDefault(); // Prevent default browser tooltip
showError(emailInput, "Please provide a valid email");
});setCustomValidity for Custom Error Messages
const usernameInput = document.getElementById("username");
usernameInput.addEventListener("input", () => {
const value = usernameInput.value;
if (value.length > 0 && value.length < 3) {
usernameInput.setCustomValidity("Username must be at least 3 characters");
} else if (/[^a-zA-Z0-9_]/.test(value)) {
usernameInput.setCustomValidity("Only letters, numbers, and underscores allowed");
} else {
usernameInput.setCustomValidity(""); // Empty string = valid
}
});
// IMPORTANT: setCustomValidity("") clears the error
// Any non-empty string means the field is invalidCustom Messages with the Constraint API
function getCustomMessage(input) {
const validity = input.validity;
if (validity.valueMissing) {
return `${input.dataset.label || "This field"} is required`;
}
if (validity.typeMismatch) {
if (input.type === "email") return "Please enter a valid email address";
if (input.type === "url") return "Please enter a valid URL";
return "Please enter a valid value";
}
if (validity.tooShort) {
return `Must be at least ${input.minLength} characters (currently ${input.value.length})`;
}
if (validity.patternMismatch) {
return input.dataset.patternMessage || "Please match the requested format";
}
if (validity.rangeUnderflow) {
return `Value must be ${input.min} or greater`;
}
if (validity.rangeOverflow) {
return `Value must be ${input.max} or less`;
}
return "Invalid value";
}
// Usage with form validation
form.addEventListener("submit", (e) => {
const inputs = form.querySelectorAll("input, select, textarea");
let firstInvalid = null;
inputs.forEach(input => {
if (!input.checkValidity()) {
showError(input, getCustomMessage(input));
if (!firstInvalid) firstInvalid = input;
} else {
clearError(input);
}
});
if (firstInvalid) {
e.preventDefault();
firstInvalid.focus(); // Focus the first invalid field
}
});Real-Time Validation
Validate as the user types or when they leave a field, not just on submit:
function setupRealTimeValidation(form) {
const inputs = form.querySelectorAll("input, textarea, select");
inputs.forEach(input => {
// Validate on blur (when user leaves the field)
input.addEventListener("blur", () => {
validateField(input);
});
// Clear error on input (while user is typing)
input.addEventListener("input", () => {
if (input.classList.contains("invalid")) {
validateField(input);
}
});
});
}
function validateField(input) {
// Use Constraint Validation API first
if (!input.checkValidity()) {
showError(input, getCustomMessage(input));
return false;
}
// Custom validation rules
if (input.dataset.validate === "email") {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!pattern.test(input.value)) {
showError(input, "Please enter a valid email");
return false;
}
}
clearError(input);
return true;
}
setupRealTimeValidation(document.getElementById("my-form"));When to Validate
| Timing | Event | Best For |
|---|---|---|
| On submit | submit | Final check before sending |
| On blur | blur | Validate when user leaves field |
| On input | input | Clear errors as user corrects mistakes |
| On change | change | Dropdowns, checkboxes, radio buttons |
The recommended approach is to validate on blur (so users are not interrupted while typing), clear errors on input (so they see corrections work), and do a final check on submit.
Password Strength Indicator
function createPasswordStrength(passwordInput, meterEl) {
passwordInput.addEventListener("input", () => {
const password = passwordInput.value;
const strength = calculateStrength(password);
meterEl.className = "strength-meter";
meterEl.classList.add(strength.level);
meterEl.style.width = `${strength.score}%`;
meterEl.textContent = strength.label;
});
}
function calculateStrength(password) {
let score = 0;
if (password.length === 0) return { score: 0, level: "none", label: "" };
if (password.length >= 8) score += 25;
if (password.length >= 12) score += 10;
if (/[A-Z]/.test(password)) score += 20;
if (/[a-z]/.test(password)) score += 15;
if (/[0-9]/.test(password)) score += 15;
if (/[^A-Za-z0-9]/.test(password)) score += 15;
if (score < 30) return { score, level: "weak", label: "Weak" };
if (score < 60) return { score, level: "fair", label: "Fair" };
if (score < 80) return { score, level: "good", label: "Good" };
return { score: 100, level: "strong", label: "Strong" };
}
const passwordInput = document.getElementById("password");
const meter = document.getElementById("strength-meter");
createPasswordStrength(passwordInput, meter);Common Validation Patterns
const patterns = {
// Phone: 10+ digits, optional +, spaces, dashes, parentheses
phone: /^[+]?[(]?[0-9]{1,4}[)]?[-\s./0-9]*$/,
// URL: http or https
url: /^https?:\/\/[^\s/$.?#].[^\s]*$/,
// Username: letters, numbers, underscores, 3-20 chars
username: /^[a-zA-Z0-9_]{3,20}$/,
// Zip code (US): 5 digits or 5+4
zipCode: /^\d{5}(-\d{4})?$/,
// Date: YYYY-MM-DD
date: /^\d{4}-\d{2}-\d{2}$/,
// No spaces
noSpaces: /^\S+$/
};
function validateWithPattern(input, patternName) {
const pattern = patterns[patternName];
if (!pattern) return true;
if (!pattern.test(input.value)) {
showError(input, `Please enter a valid ${patternName}`);
return false;
}
clearError(input);
return true;
}Real-World Example: Registration Form with Live Feedback
function createRegistrationValidator(formId) {
const form = document.getElementById(formId);
const fields = {};
const rules = {
username: [
{ test: v => v.length >= 3, message: "At least 3 characters" },
{ test: v => v.length <= 20, message: "Maximum 20 characters" },
{ test: v => /^[a-zA-Z0-9_]+$/.test(v), message: "Letters, numbers, and underscores only" }
],
email: [
{ test: v => v.includes("@"), message: "Must contain @" },
{ test: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v), message: "Enter a valid email" }
],
password: [
{ test: v => v.length >= 8, message: "At least 8 characters" },
{ test: v => /[A-Z]/.test(v), message: "One uppercase letter" },
{ test: v => /[a-z]/.test(v), message: "One lowercase letter" },
{ test: v => /[0-9]/.test(v), message: "One number" }
],
confirmPassword: [
{ test: (v, form) => v === form.querySelector("#password").value, message: "Passwords must match" }
]
};
// Build requirement lists for each field
Object.keys(rules).forEach(fieldName => {
const input = form.querySelector(`#${fieldName}`);
if (!input) return;
fields[fieldName] = { input, validated: false };
const reqList = document.createElement("ul");
reqList.className = "requirement-list";
rules[fieldName].forEach((rule, index) => {
const li = document.createElement("li");
li.textContent = rule.message;
li.dataset.index = index;
li.className = "requirement pending";
reqList.appendChild(li);
});
input.parentElement.appendChild(reqList);
// Validate on input (real-time)
input.addEventListener("input", () => {
validateFieldRules(input, fieldName, reqList);
});
// Mark as validated on blur
input.addEventListener("blur", () => {
if (input.value.trim() !== "") {
fields[fieldName].validated = true;
validateFieldRules(input, fieldName, reqList);
}
});
});
function validateFieldRules(input, fieldName, reqList) {
const fieldRules = rules[fieldName];
let allPassed = true;
fieldRules.forEach((rule, index) => {
const li = reqList.querySelector(`[data-index="${index}"]`);
const passed = rule.test(input.value, form);
li.className = `requirement ${passed ? "passed" : "failed"}`;
if (!passed) allPassed = false;
});
input.className = allPassed ? "valid" : "invalid";
return allPassed;
}
function validateAll() {
let isValid = true;
Object.keys(rules).forEach(fieldName => {
const { input } = fields[fieldName];
const reqList = input.parentElement.querySelector(".requirement-list");
if (!input.value.trim()) {
showError(input, `${fieldName} is required`);
isValid = false;
} else if (!validateFieldRules(input, fieldName, reqList)) {
isValid = false;
}
});
return isValid;
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (!validateAll()) {
const firstInvalid = form.querySelector(".invalid");
if (firstInvalid) firstInvalid.focus();
return;
}
const submitBtn = form.querySelector('[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = "Creating account...";
try {
const formData = new FormData(form);
formData.delete("confirmPassword"); // Don't send to server
const response = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(Object.fromEntries(formData))
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Registration failed");
}
form.innerHTML = `
<div class="success-banner">
<h2>Welcome aboard!</h2>
<p>Your account has been created successfully.</p>
</div>
`;
} catch (err) {
const errorBanner = form.querySelector(".form-error") || document.createElement("div");
errorBanner.className = "form-error";
errorBanner.textContent = err.message;
form.prepend(errorBanner);
submitBtn.disabled = false;
submitBtn.textContent = "Create Account";
}
});
}
createRegistrationValidator("registration-form");Rune AI
Key Insights
- Use both HTML and JavaScript: HTML attributes provide baseline validation; JavaScript adds custom rules and better error messages
- Validate on blur, clear on input: Show errors when the user leaves a field, and clear them as the user types corrections
- Constraint Validation API is built in: Use
checkValidity(),validityproperties, andsetCustomValidity()instead of reinventing validation logic - Focus the first invalid field: On submit, scroll to and focus the first field with an error so the user knows exactly where to fix
- Client-side validation is not security: Always validate on the server; JavaScript validation is purely for user experience
Frequently Asked Questions
Should I use HTML validation attributes or JavaScript validation?
How do I show errors without the default browser tooltip?
When should I validate: on input, on blur, or on submit?
How do I validate a field that depends on another field?
Is client-side validation enough for security?
Conclusion
Form validation in JavaScript gives users instant feedback that reduces frustration and prevents bad data from reaching your server. Start with the built-in Constraint Validation API that reads HTML attributes through validity properties, add setCustomValidity() for custom error messages, and layer real-time feedback using blur and input event listeners. Always validate on the server too, because client-side checks are a UX convenience, not a security measure.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.