Why JavaScript Validation: A Friendly Sign-Up Form
Bright Future Coaching is opening new batches for JEE and NEET. Hundreds of students will sign up on its website, many on a phone, many in a hurry. If a student types a 9-digit mobile number by mistake, the institute can never call them back. If the form just says “Error” after they press submit, they may give up.
A good form gives instant, friendly feedback: a clear message right under the field that has a problem, shown as soon as the student leaves that field. In the HTML Form Validation lesson you used attributes such as required, type="email" and pattern. In this lesson you add JavaScript on top, so you can write your own rules and your own messages, and control exactly when they appear.
You will learn to handle the submit event, check empty fields, emails, Indian mobile numbers and passwords, show messages next to each field, use the browser’s built-in validation tools from JavaScript and disable the submit button until the form is ready.
The submit Event and preventDefault
When a form is submitted, the browser normally sends the data and loads a new page. To check the data first, listen for the form’s submit event and call event.preventDefault(). This stops the normal sending, so your code decides what happens next.
Read each field with .value. The value is always a string. Use .trim() to remove spaces at the start and end, so a name made of only spaces counts as empty.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Enquiry Form</title>
</head>
<body>
<h2>Ask About Our Courses</h2>
<form id="enquiry">
<label for="name">Your name</label>
<input id="name">
<button type="submit">Send</button>
</form>
<p id="message"></p>
<script>
const form = document.getElementById("enquiry");
form.addEventListener("submit", function (event) {
event.preventDefault(); // stop the page from reloading
const name = document.getElementById("name").value.trim();
const message = document.getElementById("message");
if (name === "") {
message.textContent = "Please tell us your name.";
return; // stop here, do not send
}
message.textContent = "Thank you, " + name + ". We will contact you soon.";
console.log("Ready to send:", name);
});
</script>
</body>
</html>
Listen to the form’s submit event, not the button’s click event. Submit also fires when the user presses Enter inside a field, which is very common on phones.
Checking Email and Mobile Numbers
An Indian mobile number has exactly 10 digits, and the first digit is 6, 7, 8 or 9. You can check rules like this with a small pattern and its .test() method, which returns true or false.
function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function isValidMobile(mobile) {
return /^[6-9][0-9]{9}$/.test(mobile);
}
console.log(isValidEmail("asha@example.com")); // true
console.log(isValidEmail("asha@example")); // false (no dot after the @)
console.log(isValidEmail("asha example.com")); // false (space, no @)
console.log(isValidMobile("9000000000")); // true (10 digits, starts with 9)
console.log(isValidMobile("5000000000")); // false (starts with 5)
console.log(isValidMobile("900000000")); // false (only 9 digits)
console.log(isValidMobile("90000 00000")); // false (has a space)
// Be kind: remove spaces before you check
const typed = "90000 00000";
console.log(isValidMobile(typed.replaceAll(" ", ""))); // true
| Piece | Meaning |
|---|---|
^ and $ | The start and the end of the text. Together they mean “the whole value must match”. |
[6-9] | One character from 6 to 9. |
[0-9]{9} | Exactly nine digits. |
[^\s@]+ | One or more characters that are not a space and not @. |
\. | A real dot (a plain . means “any character”). |
The email pattern only checks the basic shape: some text, an @, some text, a dot, some text. That is enough to catch typing mistakes. No pattern can tell you whether an email address really exists; only sending a confirmation email can do that.
New to these patterns? The short codes between the slashes are called regular expressions. You only need the few pieces in the table here. The Regular Expressions lesson in the advanced section explains them fully.
Password Strength and Confirmation
Instead of one message like “Weak password”, tell the student exactly what is missing. Collect the problems in an array, then join them into one sentence. For the confirmation box, simply compare the two values with ===.
function passwordProblems(password) {
const problems = [];
if (password.length < 8) {
problems.push("at least 8 characters");
}
if (!/[A-Za-z]/.test(password)) {
problems.push("a letter");
}
if (!/[0-9]/.test(password)) {
problems.push("a number");
}
return problems;
}
console.log(passwordProblems("abc")); // ["at least 8 characters", "a number"]
console.log(passwordProblems("12345678")); // ["a letter"]
console.log(passwordProblems("neet2027ok")); // []
// Matching confirmation
const password = "neet2027ok";
const confirmation = "neet2027OK";
console.log(password === confirmation); // false (capital letters are different)
Messages Next to Each Field, on blur and input
Put a small empty element under every field for its message. Then decide when to check:
blurfires when the user leaves a field. This is the best moment for the first check, because the user has finished typing.inputfires on every key press. Checking here from the very first letter is annoying (“invalid email” after typing one letter), so only check while typing once the field has been visited. Then the error disappears the moment it is fixed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Field Messages</title>
<style>
body { font-family: sans-serif; padding: 16px; }
label { display: block; margin-top: 12px; }
.error { color: #b91c1c; font-size: 0.9rem; min-height: 1.2em; display: block; }
input.invalid { border: 2px solid #b91c1c; }
</style>
</head>
<body>
<label for="email">Email</label>
<input id="email" type="email" aria-describedby="email-error">
<small class="error" id="email-error"></small>
<label for="mobile">Mobile number</label>
<input id="mobile" type="tel" maxlength="10" aria-describedby="mobile-error">
<small class="error" id="mobile-error"></small>
<script>
const checks = {
email: function (value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? "" : "Please enter an email like name@example.com.";
},
mobile: function (value) {
return /^[6-9][0-9]{9}$/.test(value) ? "" : "Enter 10 digits, starting with 6, 7, 8 or 9.";
}
};
function checkField(input) {
const message = checks[input.id](input.value.trim());
document.getElementById(input.id + "-error").textContent = message;
input.classList.toggle("invalid", message !== "");
}
document.querySelectorAll("input").forEach(function (input) {
// When the user leaves the field: check it and remember it was visited
input.addEventListener("blur", function () {
input.dataset.visited = "yes";
checkField(input);
});
// While typing: check again only if the field was visited before
input.addEventListener("input", function () {
if (input.dataset.visited === "yes") {
checkField(input);
}
});
});
</script>
</body>
</html>
The aria-describedby attribute connects each input to its message, so screen readers read the message aloud too. input.dataset.visited stores a small note on the element itself (it becomes the attribute data-visited).
The Constraint Validation API
The browser already knows the HTML rules you wrote (required, type, pattern, minlength). The Constraint Validation API lets JavaScript ask the browser about them:
| Tool | What it does |
|---|---|
form.checkValidity() | Returns true if every field passes its rules. |
input.validity | An object with true or false flags: valueMissing, typeMismatch, patternMismatch, tooShort, customError and more. |
input.setCustomValidity("text") | Marks a field as invalid with your own message. An empty string "" marks it valid again. |
form.reportValidity() | Checks the form and shows the browser’s message bubbles. |
The novalidate attribute on the form stops the browser from blocking the submit by itself, so your JavaScript runs and decides. Leave all fields empty and press Sign up, then fill them one by one and watch the console.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Constraint Validation API</title>
<style>
body { font-family: sans-serif; padding: 16px; }
label { display: block; margin-top: 10px; }
</style>
</head>
<body>
<form id="signup" novalidate>
<label for="email">Email</label>
<input id="email" type="email" required>
<label for="mobile">Mobile number</label>
<input id="mobile" type="tel" required pattern="[6-9][0-9]{9}">
<label for="pw">Password</label>
<input id="pw" type="password" required minlength="8">
<label for="pw2">Confirm password</label>
<input id="pw2" type="password" required>
<p><button type="submit">Sign up</button></p>
</form>
<script>
const form = document.getElementById("signup");
const pw = document.getElementById("pw");
const pw2 = document.getElementById("pw2");
function messageFor(input) {
const v = input.validity;
if (v.valueMissing) return "This field is required.";
if (v.typeMismatch) return "Please enter a valid email address.";
if (v.patternMismatch) return "Enter 10 digits, starting with 6, 7, 8 or 9.";
if (v.tooShort) return "Use at least " + input.minLength + " characters.";
if (v.customError) return input.validationMessage;
return "OK";
}
form.addEventListener("submit", function (event) {
event.preventDefault();
// A rule HTML cannot express: the two passwords must match
pw2.setCustomValidity(pw2.value === pw.value ? "" : "Passwords do not match.");
if (!form.checkValidity()) {
form.querySelectorAll("input").forEach(function (input) {
console.log(input.id + ": " + messageFor(input));
});
form.reportValidity(); // also show the browser's own bubble
return;
}
console.log("All fields are valid. Ready to send.");
});
</script>
</body>
</html>
Notice that the pattern attribute does not need ^ and $: the browser always checks the whole value. Also, tooShort only reacts to text the user typed, not to a value set by code. One more surprise: the browser’s own email check accepts asha@example without a dot, because such addresses are allowed on private networks. If you want a dot after the @, add your own rule, as in the next example.
Real-Life Example: The Coaching Institute Sign-Up Form
Now everything comes together in the full sign-up form for Bright Future Coaching. Each field has a rule function that returns an empty string or a friendly message. Messages appear on blur and update while typing. The Create account button stays disabled until every rule passes, and the submit handler checks everything one final time.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Join Bright Future Coaching</title>
<style>
body { font-family: sans-serif; max-width: 420px; margin: 0 auto; padding: 16px; }
label { display: block; margin-top: 12px; font-weight: bold; }
input, select { width: 100%; padding: 8px; font-size: 1rem; box-sizing: border-box; }
.error { color: #b91c1c; font-size: 0.9rem; min-height: 1.2em; display: block; }
.invalid { border: 2px solid #b91c1c; }
button { margin-top: 16px; padding: 10px 20px; font-size: 1rem; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
#done { color: #166534; font-weight: bold; }
</style>
</head>
<body>
<h2>Join Bright Future Coaching</h2>
<form id="signup" novalidate>
<label for="name">Full name</label>
<input id="name" autocomplete="name" aria-describedby="name-error">
<small class="error" id="name-error"></small>
<label for="email">Email</label>
<input id="email" type="email" autocomplete="email" aria-describedby="email-error">
<small class="error" id="email-error"></small>
<label for="mobile">Mobile number (10 digits)</label>
<input id="mobile" type="tel" maxlength="10" inputmode="numeric" aria-describedby="mobile-error">
<small class="error" id="mobile-error"></small>
<label for="course">Course</label>
<select id="course" aria-describedby="course-error">
<option value="">Choose a course</option>
<option>JEE Foundation (Class 9 and 10)</option>
<option>NEET Two-Year Programme</option>
<option>Class 12 Board Revision</option>
</select>
<small class="error" id="course-error"></small>
<label for="password">Password</label>
<input id="password" type="password" autocomplete="new-password" aria-describedby="password-error">
<small class="error" id="password-error"></small>
<label for="confirm">Confirm password</label>
<input id="confirm" type="password" autocomplete="new-password" aria-describedby="confirm-error">
<small class="error" id="confirm-error"></small>
<button type="submit" id="submitBtn" disabled>Create account</button>
</form>
<p id="done" hidden></p>
<script>
const form = document.getElementById("signup");
const button = document.getElementById("submitBtn");
function passwordProblems(password) {
const problems = [];
if (password.length < 8) problems.push("at least 8 characters");
if (!/[A-Za-z]/.test(password)) problems.push("a letter");
if (!/[0-9]/.test(password)) problems.push("a number");
return problems;
}
// Each rule returns "" when the value is fine, or a friendly message
const rules = {
name: function (v) {
return v.trim().length >= 2 ? "" : "Please enter your full name.";
},
email: function (v) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()) ? "" : "Please enter an email like name@example.com.";
},
mobile: function (v) {
return /^[6-9][0-9]{9}$/.test(v.trim()) ? "" : "Enter 10 digits, starting with 6, 7, 8 or 9.";
},
course: function (v) {
return v !== "" ? "" : "Please choose a course.";
},
password: function (v) {
const problems = passwordProblems(v);
return problems.length === 0 ? "" : "Password needs " + problems.join(", ") + ".";
},
confirm: function (v) {
return v !== "" && v === form.elements.password.value ? "" : "The two passwords do not match.";
}
};
const fieldIds = Object.keys(rules);
function showResult(id) {
const input = form.elements[id];
const message = rules[id](input.value);
document.getElementById(id + "-error").textContent = message;
input.classList.toggle("invalid", message !== "");
return message === "";
}
function allValid() {
return fieldIds.every(function (id) {
return rules[id](form.elements[id].value) === "";
});
}
fieldIds.forEach(function (id) {
const input = form.elements[id];
input.addEventListener("blur", function () {
input.dataset.visited = "yes";
showResult(id);
});
input.addEventListener("input", function () {
if (input.dataset.visited === "yes") showResult(id);
// A new password can change whether the confirmation matches
if (id === "password" && form.elements.confirm.dataset.visited === "yes") showResult("confirm");
button.disabled = !allValid();
});
});
form.addEventListener("submit", function (event) {
event.preventDefault();
const results = fieldIds.map(showResult); // show every message
if (results.includes(false)) return;
const done = document.getElementById("done");
done.textContent = "Welcome, " + form.elements.name.value.trim() + "! Your seat in " +
form.elements.course.value + " is reserved.";
done.hidden = false;
form.reset();
button.disabled = true;
});
</script>
</body>
</html>
Try these changes:
- Add a rule that the name may contain only letters and spaces.
- Show a “Strong password” message in green when the password has no problems.
- Add an optional “City” field that has no rule. Make sure the button still works.
The server must check again. JavaScript in the browser is there to help honest users fix mistakes quickly. It is not security. Anyone can turn off JavaScript, change your code in DevTools or send data straight to the server without using your form at all. So the server must repeat every important check before it saves anything.
Common Mistakes
- Forgetting
preventDefault(). The page reloads and your messages vanish at once. Fix: callevent.preventDefault()at the top of the submit handler. - Checking the value without
trim(). A name of three spaces passes the “not empty” check. Fix: trim names, emails and numbers before checking. (Do not trim passwords: spaces may be part of them.) - Shouting at the user too early. Showing “Invalid email” after the first letter feels rude. Fix: check first on
blur, then oninput. - Using only colour to show errors. Some users cannot see red borders clearly. Fix: always show a text message too.
- Trusting the browser alone. Browser checks can be skipped. Fix: validate again on the server.
Practice Task: Doubt Session Booking Form
The institute wants a small form where parents can book a free doubt-clearing session. Complete the script in the starter page:
- A rule for each field: student name (at least 2 characters), parent’s mobile (10 digits starting with 6 to 9) and class (must be chosen).
- Show each message under its field when the user leaves the field.
- Keep the Book session button disabled until all three fields are valid.
- On submit, check again and show a confirmation line with the name and class.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Book a Doubt Session</title>
<style>
.error { color: #b91c1c; display: block; min-height: 1.2em; }
</style>
</head>
<body>
<h2>Book a Free Doubt Session</h2>
<form id="booking" novalidate>
<label for="student">Student name</label>
<input id="student">
<small class="error" id="student-error"></small>
<label for="phone">Parent's mobile number</label>
<input id="phone" type="tel" maxlength="10">
<small class="error" id="phone-error"></small>
<label for="grade">Class</label>
<select id="grade">
<option value="">Choose</option>
<option>9</option>
<option>10</option>
<option>11</option>
<option>12</option>
</select>
<small class="error" id="grade-error"></small>
<button type="submit" id="bookBtn" disabled>Book session</button>
</form>
<p id="done"></p>
<script>
// TODO 1: write a rules object with one function per field (student, phone, grade)
// that returns "" or a friendly message
// TODO 2: on blur, show the message under that field
// TODO 3: on input, enable the button only when every field is valid
// TODO 4: on submit, preventDefault, check all fields again and show
// "Session booked for <name>, Class <grade>." in #done
</script>
</body>
</html>
Show one possible answer (try it yourself first!)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Book a Doubt Session</title>
<style>
.error { color: #b91c1c; display: block; min-height: 1.2em; }
</style>
</head>
<body>
<h2>Book a Free Doubt Session</h2>
<form id="booking" novalidate>
<label for="student">Student name</label>
<input id="student">
<small class="error" id="student-error"></small>
<label for="phone">Parent's mobile number</label>
<input id="phone" type="tel" maxlength="10">
<small class="error" id="phone-error"></small>
<label for="grade">Class</label>
<select id="grade">
<option value="">Choose</option>
<option>9</option>
<option>10</option>
<option>11</option>
<option>12</option>
</select>
<small class="error" id="grade-error"></small>
<button type="submit" id="bookBtn" disabled>Book session</button>
</form>
<p id="done"></p>
<script>
const form = document.getElementById("booking");
const button = document.getElementById("bookBtn");
const rules = {
student: function (v) { return v.trim().length >= 2 ? "" : "Please enter the student's name."; },
phone: function (v) { return /^[6-9][0-9]{9}$/.test(v.trim()) ? "" : "Enter 10 digits, starting with 6, 7, 8 or 9."; },
grade: function (v) { return v !== "" ? "" : "Please choose a class."; }
};
const ids = Object.keys(rules);
function show(id) {
const message = rules[id](form.elements[id].value);
document.getElementById(id + "-error").textContent = message;
return message === "";
}
ids.forEach(function (id) {
const input = form.elements[id];
input.addEventListener("blur", function () { show(id); });
input.addEventListener("input", function () {
button.disabled = !ids.every(function (k) { return rules[k](form.elements[k].value) === ""; });
});
});
form.addEventListener("submit", function (event) {
event.preventDefault();
const ok = ids.map(show).every(Boolean);
if (!ok) return;
document.getElementById("done").textContent =
"Session booked for " + form.elements.student.value.trim() + ", Class " + form.elements.grade.value + ".";
form.reset();
button.disabled = true;
});
</script>
</body>
</html>
Key Takeaways
- Listen for the form’s
submitevent and callevent.preventDefault()to check data first. - Read values with
.value.trim(); check empty fields, email shape and mobile numbers with simple rules. - Show a specific message next to each field; check on
blurfirst, then oninput. - The Constraint Validation API (
checkValidity,validity,setCustomValidity) connects JavaScript to the HTML rules. - Disabling the submit button until the form is valid prevents half-filled submissions.
- Browser validation helps users; only server validation protects your data.