Every form on the web needs validation. Without it, users submit empty fields, invalid emails, passwords that are too short and phone numbers that contain letters. Your server receives garbage data and either crashes, stores bad data, or sends confusing error pages back to the user.

Good validation does two things. It prevents bad data from reaching your server. And it guides the user — clear, immediate feedback that tells them exactly what is wrong and how to fix it, right next to the field they need to correct.

This guide covers every layer of form validation: HTML5 attributes that validate automatically, CSS states that style valid and invalid fields, and JavaScript for custom rules, real-time feedback and password strength. Plus a fully working form you can copy at the end.

Why Validate — and Where

Validation happens in two places and you need both. They serve different purposes and neither one alone is enough.

Client-side validation (in the browser, using HTML and JavaScript) gives the user instant feedback. It catches mistakes before the form is submitted, saving a round trip to the server. But it is not secure — anyone can open browser DevTools and bypass client-side validation or send requests directly to your API.

Server-side validation (in your backend — Node, Flask, Django) is where security actually lives. Always validate again on the server, regardless of what the client says. Client-side validation is for user experience. Server-side validation is for security.

⚠️ Never trust the client. A user can disable JavaScript, modify the HTML in DevTools or send HTTP requests directly with Postman or curl. Client-side validation can be bypassed in seconds. Always validate on the server too.

HTML5 Built-in Validation

HTML5 added a set of attributes that tell the browser to validate a field automatically — no JavaScript needed. The browser checks them before the form is submitted and blocks submission if any field fails. This is the simplest and fastest validation to add.

required — Field Cannot Be Empty

HTML — required attribute
<!-- Blocks form submission if this field is empty --> <input type="text" name="username" required> <!-- Works on text inputs, email, password, select and textarea --> <select name="country" required> <option value="">Select a country</option> <option>India</option> <option>USA</option> </select> <textarea name="message" required></textarea>

type — Format Validation for Free

The type attribute does more than change the keyboard on mobile. It also validates the format automatically. type="email" checks for a valid email structure. type="url" checks for a valid URL. type="number" rejects letters.

HTML — input types that validate automatically
<!-- Validates email format: must contain @ and a domain --> <input type="email" name="email" required> <!-- Only accepts numbers within range --> <input type="number" name="age" min="18" max="120"> <!-- Validates URL format: must start with http:// or https:// --> <input type="url" name="website"> <!-- Date picker with optional min/max range --> <input type="date" name="dob" max="2006-01-01"> <!-- Masks input for passwords --> <input type="password" name="password" required>

minlength and maxlength — Length Constraints

HTML — length constraints on text fields
<!-- Username: between 3 and 20 characters --> <input type="text" name="username" minlength="3" maxlength="20" required > <!-- Password: at least 8 characters --> <input type="password" name="password" minlength="8" required > <!-- Textarea: message between 10 and 500 characters --> <textarea name="message" minlength="10" maxlength="500" required ></textarea>

pattern — Custom Format with Regex

The pattern attribute accepts a regular expression. The field is valid only if the value matches the pattern. This is perfect for phone numbers, postcodes, usernames that must be alphanumeric, and other custom formats.

HTML — pattern attribute with common regex patterns
<!-- Only letters, numbers and underscores, 3-20 chars --> <input type="text" name="username" pattern="[a-zA-Z0-9_]{3,20}" title="Letters, numbers and underscores only, 3–20 characters" required > <!-- Indian mobile number: starts with 6-9, 10 digits --> <input type="tel" name="phone" pattern="[6-9][0-9]{9}" title="Enter a valid 10-digit Indian mobile number" > <!-- UK postcode --> <input type="text" name="postcode" pattern="[A-Z]{1,2}[0-9]{1,2}[A-Z]? [0-9][A-Z]{2}" title="Enter a valid UK postcode" >

CSS Validation States — Visual Feedback

CSS has pseudo-classes that match the validation state of form fields. You can use them to add green borders for valid fields, red borders for invalid ones, and show or hide error icons — all without any JavaScript.

:valid and :invalid Pseudo-classes

CSS — styling valid and invalid fields
/* Base input style */ .field input, .field textarea { width: 100%; padding: 12px 16px; border: 1.5px solid #d1d5db; border-radius: 8px; font-size: 1rem; outline: none; transition: border-color .2s; } /* Green border when valid */ .field input:valid { border-color: #22c55e; } /* Red border when invalid */ .field input:invalid { border-color: #ef4444; } /* Focus state — show focus ring */ .field input:focus { border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99,102,241,.15); }

:user-invalid — Only After the User Interacted

There is a problem with :invalid. It applies immediately on page load, before the user has typed anything. An empty required field shows red the moment the page appears — before the user has even had a chance to fill it in. That is frustrating and bad UX.

:user-invalid solves this. It only activates after the user has actually interacted with the field (typed in it and moved away). Use this instead of bare :invalid for a much better experience.

CSS — :user-invalid only triggers after user interaction
/* Old way — shows red immediately on page load (bad UX) */ input:invalid { border-color: red; } /* Better way — only shows red after the user has interacted with the field */ input:user-invalid { border-color: #ef4444; } /* Show error icon after invalid interaction */ .field { position: relative; } .field input:user-invalid + .error-icon { display: block; /* reveal ✕ icon only when invalid after interaction */ } .field input:valid + .success-icon { display: block; /* reveal ✓ icon when valid */ } /* Accessibility: always show the error message alongside colour */ .error-msg { display: none; color: #ef4444; font-size: .8rem; margin-top: 4px; } input:user-invalid ~ .error-msg { display: block; }

JavaScript Validation

HTML5 attributes handle simple cases well, but JavaScript lets you write any rule you need — checking that passwords match, limiting usernames to unique values, making one field required only when another is filled in, or showing custom error messages that are clearer than the browser defaults.

The Constraint Validation API

The browser exposes a built-in JavaScript API for working with form validation. Every input element has a validity object that tells you exactly what is wrong and a checkValidity() method that returns true or false.

JavaScript — using the Constraint Validation API
const emailInput = document.querySelector('#email'); // checkValidity() — returns true if the field passes all HTML5 constraints console.log(emailInput.checkValidity()); // true or false // validity object — tells you specifically WHY the field is invalid const v = emailInput.validity; console.log(v.valueMissing); // true if required and empty console.log(v.typeMismatch); // true if value does not match type (e.g. bad email format) console.log(v.tooShort); // true if below minlength console.log(v.tooLong); // true if above maxlength console.log(v.patternMismatch); // true if value does not match pattern attribute console.log(v.rangeUnderflow); // true if below min console.log(v.rangeOverflow); // true if above max console.log(v.valid); // true if all constraints pass // Validate the whole form before submitting const form = document.querySelector('#myForm'); console.log(form.checkValidity()); // true only if ALL fields are valid

Custom Error Messages

Browser default error messages like "Please fill out this field" are generic and hard to customise per language. setCustomValidity() lets you set your own message that the browser shows in the tooltip, and it marks the field as invalid so it blocks form submission.

JavaScript — custom validation messages per field
function getEmailError(input) { const v = input.validity; if (v.valueMissing) return 'Email address is required.'; if (v.typeMismatch) return 'Please enter a valid email address (e.g. you@example.com).'; return ''; } function getUsernameError(input) { const v = input.validity; if (v.valueMissing) return 'Username is required.'; if (v.tooShort) return 'Username must be at least 3 characters long.'; if (v.patternMismatch) return 'Only letters, numbers and underscores are allowed.'; return ''; } // Display the message in your own error element rather than browser tooltip function showError(input, message) { const errorEl = input.parentElement.querySelector('.error-msg'); if (message) { errorEl.textContent = message; input.classList.add('invalid'); input.classList.remove('valid'); } else { errorEl.textContent = ''; input.classList.remove('invalid'); input.classList.add('valid'); } }

Real-Time Validation - Feedback as You Type

Showing errors only after the user clicks Submit is the bare minimum. The best forms validate in real time - showing green ticks as fields become valid and showing error messages the moment a rule is broken, without waiting for a submit attempt.

Validating on Input - As the User Types

JavaScript — validate every time the value changes
const emailInput = document.querySelector('#email'); // 'input' event fires on every keystroke emailInput.addEventListener('input', () => { const error = getEmailError(emailInput); showError(emailInput, error); }); // Check all fields when the form is submitted document.querySelector('#myForm').addEventListener('submit', (e) => { e.preventDefault(); // stop the browser from submitting // Validate all fields one more time const emailErr = getEmailError(emailInput); showError(emailInput, emailErr); // Only proceed if everything is valid if (!emailErr) { console.log('Form is valid — send to server'); } });

Validating on Blur — When the User Leaves a Field

Validating on every keystroke can be too aggressive — showing errors before the user has finished typing. A gentler approach is to validate on blur, which fires when the user clicks or tabs away from a field. This is the most common pattern in production forms.

JavaScript — blur validates when user leaves field, input clears error when fixed
const inputs = document.querySelectorAll('.validate-field'); inputs.forEach(input => { // Show error when leaving the field (blur) input.addEventListener('blur', () => { validateField(input); }); // Once the user starts fixing a broken field, clear the error as they type input.addEventListener('input', () => { if (input.classList.contains('invalid')) { validateField(input); } }); }); function validateField(input) { if (input.checkValidity()) { showError(input, ''); } else { const msg = input.dataset.errorMsg // use data-error-msg attribute if set || input.validationMessage; // fallback to browser message showError(input, msg); } }

Regex Patterns Explained

Regular expressions look intimidating but each one is just a set of rules written in a compact notation. Here are the patterns you will use most often, explained character by character.

JavaScript — common validation regex patterns with explanations
// Email — local part, @, domain, dot, TLD const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // ^ start of string // [^\s@]+ one or more chars that are NOT whitespace or @ // @ literal @ // [^\s@]+ domain name // \. literal dot // [^\s@]+$ TLD (com, org, etc.) to end of string // Strong password — uppercase + lowercase + number + special char + 8 chars const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/; // (?=.*[a-z]) lookahead: at least one lowercase letter // (?=.*[A-Z]) lookahead: at least one uppercase letter // (?=.*\d) lookahead: at least one digit // (?=.*[@$!%*?&]) lookahead: at least one special character // {8,} at least 8 characters total // Username — letters, numbers, underscores, 3-20 chars const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/; // Indian phone number — 10 digits starting with 6-9 const phoneRegex = /^[6-9]\d{9}$/; // URL — with or without www, http/https const urlRegex = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b/; // How to test a regex against a value const email = 'shashank@example.com'; console.log(emailRegex.test(email)); // true

Password Strength Meter

A password strength meter tells the user how secure their password is as they type. It checks for length, uppercase, lowercase, numbers and special characters and shows a visual bar that fills up from red to green.

JavaScript — password strength scoring and colour bar
function getPasswordStrength(password) { let score = 0; if (password.length >= 8) score++; if (password.length >= 12) score++; if (/[A-Z]/.test(password)) score++; // has uppercase if (/[a-z]/.test(password)) score++; // has lowercase if (/\d/.test(password)) score++; // has number if (/[@$!%*?&]/.test(password)) score++; // has special char if (score <= 2) return { label: 'Weak', pct: 33, color: '#ef4444' }; if (score <= 4) return { label: 'Medium', pct: 66, color: '#f59e0b' }; return { label: 'Strong', pct: 100, color: '#22c55e' }; } const pwInput = document.querySelector('#password'); const strengthBar = document.querySelector('.strength-bar'); const strengthText = document.querySelector('.strength-label'); pwInput.addEventListener('input', () => { const result = getPasswordStrength(pwInput.value); strengthBar.style.width = result.pct + '%'; strengthBar.style.background = result.color; strengthText.textContent = pwInput.value ? result.label : ''; });
Live Demo — try typing a password below

Accessibility — Error Messages That Work for Everyone

Colour alone is not enough to communicate an error. Some users are colour-blind. Screen readers do not read visual styling. Accessible form validation requires text error messages, ARIA attributes and making sure error messages are announced to screen readers.

HTML — accessible form field with ARIA error linking
<!-- Link the error message to the input using aria-describedby --> <div class="field"> <label for="email">Email address</label> <input type="email" id="email" name="email" required aria-describedby="email-error" aria-invalid="false" > <<!-- role="alert" announces the error immediately to screen readers --> <span id="email-error" class="error-msg" role="alert"></span> </div>
JavaScript — update aria-invalid when state changes
function showError(input, message) { const errorEl = document.getElementById(input.getAttribute('aria-describedby')); if (message) { errorEl.textContent = message; input.setAttribute('aria-invalid', 'true'); input.classList.replace('valid', 'invalid'); } else { errorEl.textContent = ''; input.setAttribute('aria-invalid', 'false'); input.classList.replace('invalid', 'valid'); } }

Complete Working Form

Here is a fully working registration form that brings together everything in this guide — HTML5 attributes, CSS states, real-time JavaScript validation, password strength meter and accessible error messages. All in one file you can copy and use.

Live Demo — try submitting this form
HTML — complete registration form structure
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Registration Form</title> <link rel="stylesheet" href="style.css"> </head> <body> <form id="registerForm" novalidate> <h2>Create your account</h2> <div class="field"> <label for="name">Full name</label> <input type="text" id="name" name="name" minlength="2" required aria-describedby="name-error" placeholder="Your full name"> <span id="name-error" class="error-msg" role="alert"></span> </div> <div class="field"> <label for="email">Email address</label> <input type="email" id="email" name="email" required aria-describedby="email-error" placeholder="you@example.com"> <span id="email-error" class="error-msg" role="alert"></span> </div> <div class="field"> <label for="password">Password</label> <input type="password" id="password" name="password" minlength="8" required aria-describedby="password-error"> <div class="strength-wrap"> <div class="strength-bar"></div> </div> <span class="strength-label"></span> <span id="password-error" class="error-msg" role="alert"></span> </div> <div class="field"> <label for="confirm">Confirm password</label> <input type="password" id="confirm" name="confirm" required aria-describedby="confirm-error"> <span id="confirm-error" class="error-msg" role="alert"></span> </div> <button type="submit">Create account</button> </form> <script src="validate.js"></script> </body></html>
JavaScript (validate.js) — complete validation logic
// ── Helpers ──────────────────────────────────────────── function showError(inputId, message) { const input = document.getElementById(inputId); const errorEl = document.getElementById(inputId + '-error'); errorEl.textContent = message; input.className = 'field-input ' + (message ? 'invalid' : 'valid'); input.setAttribute('aria-invalid', message ? 'true' : 'false'); return !message; } // ── Individual validators ────────────────────────────── function validateName() { const val = document.getElementById('name').value.trim(); if (!val) return showError('name', 'Full name is required.'); if (val.length < 2) return showError('name', 'Name must be at least 2 characters.'); return showError('name', ''); } function validateEmail() { const val = document.getElementById('email').value.trim(); const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!val) return showError('email', 'Email is required.'); if (!regex.test(val)) return showError('email', 'Enter a valid email (e.g. you@example.com).'); return showError('email', ''); } function validatePassword() { const val = document.getElementById('password').value; if (!val) return showError('password', 'Password is required.'); if (val.length < 8) return showError('password', 'Password must be at least 8 characters.'); return showError('password', ''); } function validateConfirm() { const pw = document.getElementById('password').value; const conf = document.getElementById('confirm').value; if (!conf) return showError('confirm', 'Please confirm your password.'); if (pw !== conf) return showError('confirm', 'Passwords do not match.'); return showError('confirm', ''); } // ── Wire up blur and input events ───────────────────── document.getElementById('name') .addEventListener('blur', validateName); document.getElementById('email') .addEventListener('blur', validateEmail); document.getElementById('password').addEventListener('blur', validatePassword); document.getElementById('confirm') .addEventListener('blur', validateConfirm); document.getElementById('name') .addEventListener('input', validateName); document.getElementById('email') .addEventListener('input', validateEmail); // ── Submit handler ──────────────────────────────────── document.getElementById('registerForm').addEventListener('submit', (e) => { e.preventDefault(); const ok = validateName() && validateEmail() && validatePassword() && validateConfirm(); if (ok) console.log('All valid — send to server!'); });

Client-Side vs Server-Side — Summary

AspectClient-side (HTML + JS)Server-side (Backend)
PurposeUser experience — instant feedbackSecurity — prevent bad data
Can be bypassedYes — DevTools, curl, PostmanNo — always runs server-side
SpeedInstant — no server tripRequires HTTP round trip
RequiredOptional but strongly recommendedAlways mandatory
Error displayInline next to the fieldUsually on the next page or via API

Quick Reference

TechniqueHow to Use ItBest For
requiredHTML attributeAny field that must not be empty
type="email"HTML attributeEmail format validation for free
minlength / maxlengthHTML attributesString length constraints
min / maxHTML attributes on number/dateNumeric and date range constraints
patternHTML attribute + regex stringCustom format (phone, username, postcode)
:user-invalidCSS pseudo-classShow red border after user interacted
:validCSS pseudo-classShow green border when correct
checkValidity()JS method on inputCheck if a field passes all HTML5 rules
validity objectJS property on inputFind out specifically why a field failed
setCustomValidity()JS method on inputCustom browser tooltip message
blur eventJS addEventListenerValidate when user leaves a field
input eventJS addEventListenerLive feedback on every keystroke
aria-invalid + role="alert"HTML attributesAccessible errors for screen readers

⚡ Key Takeaways
  • Client-side validation is for user experience. Server-side validation is for security. You always need both. Client-side can be bypassed in seconds — never trust user input without re-validating on the server.
  • HTML5 validation attributes like required, type="email", minlength, maxlength and pattern validate automatically with zero JavaScript and block form submission when rules are broken.
  • Use novalidate on the form element if you want to suppress browser default error tooltips and show your own custom error messages in the UI instead.
  • :user-invalid is better than :invalid for styling because it only activates after the user has actually interacted with the field. Bare :invalid shows red borders on page load before anyone has typed anything.
  • The Constraint Validation API gives you checkValidity() and the validity object. The validity object tells you exactly which rule failed — valueMissing, typeMismatch, tooShort, patternMismatch and so on.
  • The best UX pattern: validate on blur (when the user leaves a field) to show initial errors, and on input to clear errors as the user fixes them. Do not show errors before the user has touched a field.
  • Always use text error messages alongside colours. Colour alone fails colour-blind users and screen readers. Use aria-describedby to link error messages to their field, role="alert" to announce them to screen readers, and update aria-invalid to true/false as state changes.
  • A password strength meter scores against length, uppercase, lowercase, numbers and special characters. Return a percentage and colour (red/amber/green) based on the score and update a visual bar in real time on every keystroke.
  • Never validate password match using the pattern attribute — you cannot reference another field's value in HTML. Do this in JavaScript: compare password.value === confirm.value on blur.
  • Run all validators one final time on submit even if you have real-time validation. This catches any field the user skipped entirely.