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.

JavaScriptbeginner
11 min read

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?

BenefitDescription
Instant feedbackUsers see errors immediately without waiting for a server response
Reduced server loadInvalid submissions never reach the server
Better UXHighlights exactly which field needs fixing
Faster correctionsUsers 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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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 called

Validity Properties Reference

PropertyTriggerHTML Attribute
valueMissingField is emptyrequired
typeMismatchWrong formattype="email" or type="url"
tooShortBelow minimum lengthminlength="8"
tooLongAbove maximum lengthmaxlength="50"
patternMismatchFails regex patternpattern="[A-Za-z]+"
rangeUnderflowNumber below minimummin="1"
rangeOverflowNumber above maximummax="100"
stepMismatchNumber not matching stepstep="5"
customErrorCustom message setsetCustomValidity()

checkValidity and reportValidity

javascriptjavascript
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

javascriptjavascript
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 invalid

Custom Messages with the Constraint API

javascriptjavascript
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:

javascriptjavascript
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

TimingEventBest For
On submitsubmitFinal check before sending
On blurblurValidate when user leaves field
On inputinputClear errors as user corrects mistakes
On changechangeDropdowns, 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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

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(), validity properties, and setCustomValidity() 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
RunePowered by Rune AI

Frequently Asked Questions

Should I use HTML validation attributes or JavaScript validation?

Use both. HTML attributes (`required`, `minlength`, `pattern`, `type="email"`) provide free browser-level validation. JavaScript builds on top with custom rules, better error messages, and real-time feedback. The Constraint Validation API bridges both by letting you read HTML validation state with `input.validity` and add custom errors with `setCustomValidity()`.

How do I show errors without the default browser tooltip?

Call `event.preventDefault()` in the `invalid` event listener for each input. This suppresses the browser's built-in popup. Then display your own error message in a custom element. Alternatively, add `novalidate` to the `<form>` element to disable all browser validation UI, then handle everything with JavaScript using `checkValidity()`.

When should I validate: on input, on blur, or on submit?

Use a layered approach. Validate on `blur` (when the user leaves a field) for initial feedback. Clear errors on `input` (while typing) so the user sees corrections work. Run a final check on `submit` to catch anything missed. This avoids annoying users while they are still typing but catches errors before [form submission](/tutorials/programming-languages/javascript/javascript-form-handling-and-submission-tutorial).

How do I validate a field that depends on another field?

Read the other field's value inside your validation function. For example, a "confirm password" check reads both password [input values](/tutorials/programming-languages/javascript/javascript-form-handling-and-submission-tutorial). For fields like "end date must be after start date," compare the two values in the same handler. You can also use `setCustomValidity()` on one field and trigger re-validation of both fields when either changes.

Is client-side validation enough for security?

No. Client-side validation is purely for user experience. JavaScript can be disabled, and network requests can be crafted manually to bypass any client-side check. Always validate and sanitize data on the server. Treat client-side validation as a helpful first filter, not a security boundary.

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.