JavascriptAdvanced
Form Validation
Build a multi-field form with real-time validation that shows errors inline and only submits when all fields are valid.
Form Validation
Problem
Build a registration form with real-time validation. Each field is validated when the user leaves it (blur) and errors are shown inline next to the field. The form only submits when every field passes validation.
User clicks submit without filling anything
→ All fields show their error messages
User types "a" in username and moves to next field
→ "Username must be at least 3 characters"
User types "notanemail" in email and moves on
→ "Please enter a valid email address"
User types "pass" in password
→ "Password must be at least 8 characters"
User types "differentpass" in confirm password
→ "Passwords do not match"
All fields valid → form submits → success message shownLogic
- Define validation rules for each field
- Validate each field on
blur— when the user leaves it - Also validate on
input— only if the field already has an error shown - Show error message below the field and add an error style
- On form submit — validate all fields at once
- If any field fails — stop submission and show all errors
- If all pass — show a success message
Flow
HTML Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Form Validation</title>
<style>
body {
font-family: sans-serif;
max-width: 460px;
margin: 60px auto;
padding: 0 20px;
}
/* each field group — label, input, error */
.field-group {
margin-bottom: 20px;
}
.field-group label {
display: block;
font-weight: 500;
margin-bottom: 6px;
color: #333;
font-size: 0.9rem;
}
.field-group input {
width: 100%;
padding: 12px;
font-size: 1rem;
border: 2px solid #ccc;
border-radius: 8px;
outline: none;
box-sizing: border-box;
transition: border-color 0.2s;
}
.field-group input:focus {
border-color: #555;
}
/* valid field — green border */
.field-group input.valid {
border-color: #1a7a3f;
}
/* invalid field — red border */
.field-group input.invalid {
border-color: #c0392b;
}
/* error message below the input */
.error-message {
font-size: 0.82rem;
color: #c0392b;
margin-top: 5px;
min-height: 18px;
display: block;
}
button[type="submit"] {
width: 100%;
padding: 14px;
font-size: 1rem;
font-weight: 600;
background: #111;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
margin-top: 6px;
}
button[type="submit"]:hover {
opacity: 0.85;
}
/* success message shown after valid submission */
#success {
display: none;
background: #e6f9ee;
color: #1a7a3f;
border-radius: 8px;
padding: 20px;
text-align: center;
font-weight: 500;
}
</style>
</head>
<body>
<h1>Create Account</h1>
<!-- success message — shown on valid submission -->
<div id="success">
✅ Account created successfully! Welcome aboard.
</div>
<form id="register-form" novalidate>
<!-- username -->
<div class="field-group">
<label for="username">Username</label>
<input
type="text"
id="username"
placeholder="At least 3 characters, no spaces"
/>
<span class="error-message" id="username-error"></span>
</div>
<!-- email -->
<div class="field-group">
<label for="email">Email</label>
<input
type="email"
id="email"
placeholder="your@email.com"
/>
<span class="error-message" id="email-error"></span>
</div>
<!-- password -->
<div class="field-group">
<label for="password">Password</label>
<input
type="password"
id="password"
placeholder="At least 8 characters"
/>
<span class="error-message" id="password-error"></span>
</div>
<!-- confirm password -->
<div class="field-group">
<label for="confirm-password">Confirm Password</label>
<input
type="password"
id="confirm-password"
placeholder="Repeat your password"
/>
<span class="error-message" id="confirm-password-error"></span>
</div>
<button type="submit">Create Account</button>
</form>
<script src="script.js"></script>
</body>
</html>Solution
// Step 1 — select form and inputs
const form = document.querySelector("#register-form");
const successMessage = document.querySelector("#success");
const fields = {
username: document.querySelector("#username"),
email: document.querySelector("#email"),
password: document.querySelector("#password"),
confirmPassword: document.querySelector("#confirm-password"),
};
// Step 2 — define validation rules for each field
// each rule returns null if valid or an error message string if invalid
const validators = {
username(value) {
if (!value) return "Username is required.";
if (value.length < 3) return "Username must be at least 3 characters.";
if (value.length > 20) return "Username must be 20 characters or less.";
if (/\s/.test(value)) return "Username cannot contain spaces.";
if (!/^[a-zA-Z0-9_]+$/.test(value))
return "Username can only contain letters, numbers, and underscores.";
return null; // null means valid
},
email(value) {
if (!value) return "Email is required.";
// basic email regex — checks for something@something.something
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
return "Please enter a valid email address.";
return null;
},
password(value) {
if (!value) return "Password is required.";
if (value.length < 8) return "Password must be at least 8 characters.";
if (!/[A-Z]/.test(value))
return "Password must contain at least one uppercase letter.";
if (!/[0-9]/.test(value))
return "Password must contain at least one number.";
return null;
},
confirmPassword(value) {
if (!value) return "Please confirm your password.";
// compare against the password field's current value
if (value !== fields.password.value) return "Passwords do not match.";
return null;
},
};
// Step 3 — validate a single field by name
// returns true if valid, false if not
function validateField(name) {
const input = fields[name];
const errorEl = document.querySelector(`#${toKebab(name)}-error`);
const value = input.value.trim();
// run the validator for this field
const error = validators[name](value);
if (error) {
// Step 4 — show error state
input.classList.remove("valid");
input.classList.add("invalid");
errorEl.textContent = error;
return false;
} else {
// Step 5 — show valid state
input.classList.remove("invalid");
input.classList.add("valid");
errorEl.textContent = "";
return true;
}
}
// helper — converts camelCase to kebab-case
// "confirmPassword" → "confirm-password"
// needed to match the HTML id attributes
function toKebab(str) {
return str.replace(/([A-Z])/g, "-$1").toLowerCase();
}
// Step 6 — attach blur listeners to all fields
// validate when user leaves a field
Object.keys(fields).forEach((name) => {
fields[name].addEventListener("blur", () => {
validateField(name);
});
// Step 7 — also validate on input — but only if the field already has an error
// this gives live feedback once the user has already triggered validation
fields[name].addEventListener("input", () => {
if (fields[name].classList.contains("invalid")) {
validateField(name);
}
});
});
// Step 8 — handle form submission
form.addEventListener("submit", (e) => {
// always prevent default — we control submission
e.preventDefault();
// Step 9 — validate all fields at once
const results = Object.keys(fields).map((name) => validateField(name));
// Step 10 — check if all passed
const allValid = results.every(Boolean);
if (!allValid) {
// find the first invalid field and focus it
const firstInvalid = Object.keys(fields).find(
(name) => fields[name].classList.contains("invalid")
);
if (firstInvalid) fields[firstInvalid].focus();
return;
}
// Step 11 — all valid — show success
form.style.display = "none";
successMessage.style.display = "block";
});Code Execution
Trace through submitting with empty fields:
| Field | Value | Validator result | Outcome |
|---|---|---|---|
| username | "" | "Username is required." | ❌ invalid |
"" | "Email is required." | ❌ invalid | |
| password | "" | "Password is required." | ❌ invalid |
| confirmPassword | "" | "Please confirm your password." | ❌ invalid |
allValid | [false, false, false, false].every(Boolean) | false | submission blocked |
Trace through username "a" on blur:
| Step | Code | Result |
|---|---|---|
| Value | "a".trim() | "a" |
| Run validator | validators.username("a") | "Username must be at least 3 characters." |
| Apply error | input.classList.add("invalid") | red border |
| Show message | errorEl.textContent = "..." | error shown below |
Trace through all valid fields:
| Field | Value | Result |
|---|---|---|
| username | "ali_dev" | ✅ valid |
"ali@example.com" | ✅ valid | |
| password | "Password1" | ✅ valid |
| confirmPassword | "Password1" | ✅ valid |
allValid | [true,true,true,true].every(Boolean) | true |
| Action | form hidden, success shown | ✅ |
Output
Submit empty form → All 4 fields show error messages
Type "a" in username → "Username must be at least 3 characters"
Type valid username → Green border, error cleared
Type "notanemail" → "Please enter a valid email address"
Passwords do not match → "Passwords do not match"
All fields valid + submit → Form hidden, success message shown