It is very important to pre-validate the forms before submission as they can have inappropriate values. JavaScript provides the facility to validate the form on the client-side so data processing will be faster than server-side validation.
Client-side validation should be treated as a user-experience feature, not a security boundary. Users can disable JavaScript, modify requests, or call your API directly, so the server must validate the same rules again before saving data.
The old-style example above returns false to stop form submission. For new projects, prefer addEventListener(), clear error messages near each field, and the HTML5 Constraint Validation API when possible.
function validate() {
const form = document.forms.myForm;
const name = form.name.value.trim();
const email = form.email.value.trim();
const password = form.password.value;
if (name === "") {
alert("Please enter your name.");
form.name.focus();
return false;
}
if (email === "") {
alert("Please enter your email.");
form.email.focus();
return false;
}
if (password.length < 8) {
alert("Please enter a password with at least 8 characters.");
form.password.focus();
return false;
}
return true;
}
A good validation flow tells users what went wrong, places the message near the input, and exposes the message to screen readers. The example below uses aria-describedby in HTML and writes the error text into matching message elements.
<form id="signupForm" novalidate>
<label for="email">Email</label>
<input id="email" name="email" type="email" aria-describedby="emailError">
<small id="emailError" class="error" aria-live="polite"></small>
<label for="password">Password</label>
<input id="password" name="password" type="password" aria-describedby="passwordError">
<small id="passwordError" class="error" aria-live="polite"></small>
<button type="submit">Create account</button>
</form>
const form = document.querySelector("#signupForm");
const email = document.querySelector("#email");
const password = document.querySelector("#password");
function showError(input, message) {
const error = document.querySelector(`#${input.id}Error`);
input.setAttribute("aria-invalid", "true");
error.textContent = message;
}
function clearError(input) {
const error = document.querySelector(`#${input.id}Error`);
input.removeAttribute("aria-invalid");
error.textContent = "";
}
form.addEventListener("submit", function (event) {
let isValid = true;
clearError(email);
clearError(password);
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim())) {
showError(email, "Enter a valid email address.");
isValid = false;
}
if (password.value.length < 8) {
showError(password, "Password must be at least 8 characters.");
isValid = false;
}
if (!isValid) {
event.preventDefault();
}
});
Regular expressions provide powerful pattern matching for validating emails, phone numbers, URLs, and more.
// Email validation
function isValidEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
console.log(isValidEmail('user@example.com')); // true
console.log(isValidEmail('invalid-email')); // false
// Phone number (10 digits)
function isValidPhone(phone) {
return /^\d{10}$/.test(phone);
}
console.log(isValidPhone('9876543210')); // true
// Strong password: min 8 chars, uppercase, lowercase, digit, special char
function isStrongPassword(pwd) {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/.test(pwd);
}
console.log(isStrongPassword('Secure@123')); // true
console.log(isStrongPassword('weak')); // false
// URL validation
function isValidURL(url) {
try {
new URL(url);
return true;
} catch {
return false;
}
}
console.log(isValidURL('https://tutorialslogic.com')); // true
Modern browsers provide a built-in Constraint Validation API that works alongside HTML5 form attributes like required, minlength, pattern, and type.
const form = document.getElementById('myForm');
const emailInput = document.getElementById('email');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Check validity using built-in API
if (!emailInput.validity.valid) {
if (emailInput.validity.valueMissing) {
emailInput.setCustomValidity('Email is required.');
} else if (emailInput.validity.typeMismatch) {
emailInput.setCustomValidity('Please enter a valid email address.');
}
emailInput.reportValidity();
return;
}
// Clear custom message and submit
emailInput.setCustomValidity('');
console.log('Form is valid, submitting...');
});
// Real-time validation feedback
emailInput.addEventListener('input', function() {
if (this.validity.valid) {
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid');
this.classList.add('is-invalid');
}
});
Start with semantic HTML controls and attributes such as `required`, `type`, `min`, `max`, `step`, `minlength`, `maxlength`, and `pattern`. The browser can prevent invalid submission, expose input modes, and provide a baseline without custom JavaScript. Use the type that matches the value’s meaning, but remember that an email input checks syntax rather than whether the mailbox exists.
The `validity` property exposes flags including `valueMissing`, `typeMismatch`, `patternMismatch`, range and length failures, `stepMismatch`, `badInput`, and `customError`. `checkValidity()` returns the current result and dispatches invalid events as specified; `reportValidity()` also asks the browser to present the failure. Read the specific flag when custom text must explain the correction.
`setCustomValidity(message)` marks a control invalid while the message is non-empty. Clear it with an empty string as soon as the custom rule passes, or the field remains invalid after the user corrects it. Use custom validity for cross-field or domain rules that belong to the control, not to replace every native message automatically.
The `novalidate` form attribute disables interactive native validation for that form, and `formnovalidate` can affect one submit button. Use them only when the application implements an accessible alternative. Disabling browser presentation without replacing focus, messages, and invalid-state semantics creates a worse form.
Validate format after the user has had a reasonable chance to enter it. Showing errors on the first keystroke creates noise, especially for dates, names, and confirmation fields. A common design validates on blur after interaction, updates an existing error on input, and validates every field on submission.
Listen for the `submit` event on the form, not only a button click, because Enter and `requestSubmit()` can submit too. An invalid form may trigger `invalid` events without firing `submit`. Calling `form.submit()` bypasses the submit event and constraint validation; use `requestSubmit()` when script should follow the normal submission path.
Prevent the default action only when JavaScript owns submission. Disable or lock repeated submission while a request is pending, but restore controls after failure and preserve the clicked submitter when different buttons represent different actions. `FormData` includes successful named controls according to form rules; inspect that payload rather than assuming every visible field is present.
Normalize only what the domain permits. Trimming surrounding whitespace may be valid for an email address but not for a password. Parse numbers and dates with an explicit format and reject ambiguous partial conversion. Keep the original user input available when redisplaying a server error so correction does not require retyping unrelated fields.
Associate every control with a visible label and identify required fields in text or semantics, not color alone. Place a concise correction message near the field and connect it with `aria-describedby` when appropriate. Set `aria-invalid` to reflect the current error state only when custom behavior needs it.
After a failed submission, focus the first invalid control or an error summary that links to invalid fields according to the form’s complexity. Do not move focus on every keystroke. Announce newly added summary or status text through a suitable live region, avoiding repeated announcements each time a character changes.
Write messages that state the field and the correction: “End date must be on or after start date” is better than “Invalid value.” Preserve the label, instructions, allowed units, and example format. Do not expose account existence or internal validation details where that creates a privacy or enumeration risk.
Use more than red borders. Icons, text, and valid semantic relationships must survive high contrast, zoom, screen readers, keyboard navigation, and mobile layout. Test native and custom messages in the browsers and assistive technologies the product supports, including the reduced-JavaScript path when relevant.
Syntactic validation checks shape; semantic validation checks meaning in context. A date can be syntactically valid but outside the booking window. A product ID can match a pattern but not exist or belong to the current tenant. Keep cross-field and domain rules in named functions with stable error codes so client and server behavior can be compared.
Asynchronous checks such as username availability are hints until the server commits the operation. Debounce requests, abort stale checks, identify the value being checked, and show an indeterminate state. A name reported available can be taken by another request before submission, so the server must enforce uniqueness atomically and return a field-level conflict.
Client validation improves feedback but is never a security boundary. Attackers can disable scripts or construct requests directly. Validate every untrusted input on the server before using it, including hidden fields, select values, headers, files, partner feeds, and API payloads. Prefer an allowlist of valid values and ranges over attempts to block a growing list of dangerous strings.
Validation does not replace context-aware output encoding, parameterized database queries, authorization, CSRF protection, safe file handling, or rate limits. Preserve legitimate Unicode and punctuation required by the domain. For uploads, verify size, detected content, permitted type, safe storage name, scanning policy, and authorization instead of trusting extension or client MIME type.
Create a validation matrix from each field’s accepted type, required state, normalization, minimum, maximum, allowed set, cross-field rule, and server error code. Test one valid representative, every boundary, missing and empty forms, wrong types, malformed Unicode where relevant, and combinations that change domain meaning. Generated random inputs can supplement but not replace known business boundaries.
Keep a single authoritative contract where practical, such as a server schema that can generate client hints or shared language-neutral fixtures. Client and server implementations can still differ because the browser needs presentation while the server needs trust. Run contract tests that submit the same fixtures to both and flag disagreements rather than copying regular expressions manually and assuming they remain synchronized.
Test request tampering outside the UI: remove required keys, add unknown keys, alter hidden values, send duplicate parameters, exceed size limits, change content type, and submit values from another tenant. Confirm the server rejects safely, returns stable field or form codes, avoids echoing unsafe text, and records high-risk tampering without filling logs with sensitive payloads.
Explore 500+ free tutorials across 20+ languages and frameworks.