Remote Income Hub

How to Validate a Form with Plain JavaScript (No Libraries)

Form validation libraries are useful for complex applications. But for most contact forms, signup pages, and checkout flows, plain JavaScript gives you everything you need — and no extra dependencies, no bundle size cost, and no abstraction layer between you and what is actually happening.

This guide builds a complete, production-ready form validator from scratch. By the end you will have reusable validation logic, clean error messages, accessible markup, and a pattern you can adapt to any form.

What We’re Building

A signup form with these fields:

Each field validates on blur when the user leaves it, and again on submit. Errors appear inline below each field and clear when the user corrects their input.

The HTML


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Form Validation</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>

  <div class="form-wrapper">
    <h1>Create Account</h1>

    <form id="signup-form" novalidate>

      <div class="form-group">
        <label for="name">Full Name</label>
        <input type="text" id="name" name="name" autocomplete="name" />
        <span class="error" id="name-error"></span>
      </div>

      <div class="form-group">
        <label for="email">Email Address</label>
        <input type="email" id="email" name="email" autocomplete="email" />
        <span class="error" id="email-error"></span>
      </div>

      <div class="form-group">
        <label for="password">Password</label>
        <input type="password" id="password" name="password" autocomplete="new-password" />
        <span class="error" id="password-error"></span>
      </div>

      <div class="form-group">
        <label for="confirm-password">Confirm Password</label>
        <input type="password" id="confirm-password" name="confirm-password" autocomplete="new-password" />
        <span class="error" id="confirm-password-error"></span>
      </div>

      <button type="submit">Create Account</button>

    </form>
  </div>

  <script src="validate.js"></script>
</body>
</html>

The novalidate attribute on the form disables browser-native validation so you have full control over error messages and timing. You handle everything in JavaScript.

The CSS


* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f1f5f9;
  font-family: "Segoe UI", sans-serif;
}

.form-wrapper {
  background: #fff;
  padding: 40px;
  border-radius: 12px;
  width: 100%;
  max-width: 460px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}

h1 {
  font-size: 1.5rem;
  margin-bottom: 28px;
  color: #1e293b;
}

.form-group {
  margin-bottom: 20px;
}

label {
  display: block;
  font-size: 0.875rem;
  font-weight: 600;
  color: #374151;
  margin-bottom: 6px;
}

input {
  width: 100%;
  padding: 10px 14px;
  border: 1.5px solid #d1d5db;
  border-radius: 8px;
  font-size: 1rem;
  color: #1e293b;
  outline: none;
  transition: border-color 0.2s;
}

input:focus {
  border-color: #3b82f6;
}

input.invalid {
  border-color: #ef4444;
}

input.valid {
  border-color: #22c55e;
}

.error {
  display: block;
  font-size: 0.8rem;
  color: #ef4444;
  margin-top: 5px;
  min-height: 18px;
}

button[type="submit"] {
  width: 100%;
  padding: 12px;
  background: #3b82f6;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  margin-top: 8px;
  transition: background 0.2s;
}

button[type="submit"]:hover {
  background: #2563eb;
}

The JavaScript


// ─── Validator Rules ────────────────────────────────────────────────

const validators = {
  name(value) {
    if (!value.trim()) return "Full name is required.";
    if (value.trim().length < 2) return "Name must be at least 2 characters.";
    return null;
  },

  email(value) {
    if (!value.trim()) return "Email address is required.";
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!regex.test(value.trim())) 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 (!/\d/.test(value)) return "Password must include at least one number.";
    return null;
  },

  "confirm-password"(value) {
    const password = document.getElementById("password").value;
    if (!value) return "Please confirm your password.";
    if (value !== password) return "Passwords do not match.";
    return null;
  },
};

// ─── Helper Functions ────────────────────────────────────────────────

function getElements(fieldName) {
  return {
    input: document.getElementById(fieldName),
    error: document.getElementById(`${fieldName}-error`),
  };
}

function showError(input, errorEl, message) {
  input.classList.remove("valid");
  input.classList.add("invalid");
  errorEl.textContent = message;
}

function showValid(input, errorEl) {
  input.classList.remove("invalid");
  input.classList.add("valid");
  errorEl.textContent = "";
}

function validateField(fieldName) {
  const { input, error } = getElements(fieldName);
  const validate = validators[fieldName];

  if (!validate) return true;

  const message = validate(input.value);

  if (message) {
    showError(input, error, message);
    return false;
  }

  showValid(input, error);
  return true;
}

// ─── Attach Blur Listeners ───────────────────────────────────────────

const fieldNames = ["name", "email", "password", "confirm-password"];

fieldNames.forEach(name => {
  const input = document.getElementById(name);
  input.addEventListener("blur", () => validateField(name));
});

// Re-validate confirm password when password changes
document.getElementById("password").addEventListener("input", () => {
  const confirmInput = document.getElementById("confirm-password");
  if (confirmInput.value) validateField("confirm-password");
});

// ─── Form Submit ─────────────────────────────────────────────────────

document.getElementById("signup-form").addEventListener("submit", function (event) {
  event.preventDefault();

  const results = fieldNames.map(name => validateField(name));
  const allValid = results.every(Boolean);

  if (!allValid) {
    // Focus the first invalid field
    const firstInvalid = fieldNames.find(name => !validators[name](
      document.getElementById(name).value
    ));
    if (firstInvalid) document.getElementById(firstInvalid).focus();
    return;
  }

  // All fields are valid — proceed
  console.log("Form submitted successfully.");
  // submitToServer({ name, email, password });
});

How It Works

The validators object maps each field name to a function that takes the field’s value and returns either an error message string or null if the value is valid. Adding a new field means adding one entry to this object — no other changes required.

validateField() looks up the validator for a given field, runs it against the current value, and calls showError() or showValid() based on the result. It returns true for valid and false for invalid — the return value is used during submit to determine whether the form is ready.

Blur validation runs when the user leaves each field. This gives immediate feedback at the right moment — not while they’re still typing, but as soon as they move on.

Submit validation runs all fields at once by mapping validateField() across every field name. If anything fails, the first invalid field receives focus so the user knows exactly where to look.

Re-validating confirm password when the password field changes prevents a stale mismatch error from persisting after the user corrects their password.

Extending the Validator

Adding a phone number field takes three steps:

1. Add the HTML:


<div class="form-group">
  <label for="phone">Phone Number</label>
  <input type="tel" id="phone" name="phone" />
  <span class="error" id="phone-error"></span>
</div>

2. Add the validator rule:


phone(value) {
  if (!value.trim()) return "Phone number is required.";
  const regex = /^\+?[\d\s\-().]{7,15}$/;
  if (!regex.test(value.trim())) return "Please enter a valid phone number.";
  return null;
},

3. Add it to the field names array:


const fieldNames = ["name", "email", "phone", "password", "confirm-password"];

That is all. The blur listener, the submit validation, and the error/valid state handling all work automatically because the pattern is consistent.

Making It Reusable Across Multiple Forms

Here is a class-based version that encapsulates everything and works on any form:


class FormValidator {
  constructor(formId, rules) {
    this.form = document.getElementById(formId);
    this.rules = rules;
    this.fieldNames = Object.keys(rules);
    this.init();
  }

  validate(fieldName) {
    const input = this.form.querySelector(`[name="${fieldName}"]`);
    const error = this.form.querySelector(`#${fieldName}-error`);
    const rule = this.rules[fieldName];

    if (!input || !rule) return true;

    const message = rule(input.value, this.form);

    if (message) {
      input.classList.remove("valid");
      input.classList.add("invalid");
      if (error) error.textContent = message;
      return false;
    }

    input.classList.remove("invalid");
    input.classList.add("valid");
    if (error) error.textContent = "";
    return true;
  }

  init() {
    this.fieldNames.forEach(name => {
      const input = this.form.querySelector(`[name="${name}"]`);
      if (input) {
        input.addEventListener("blur", () => this.validate(name));
      }
    });

    this.form.addEventListener("submit", (event) => {
      event.preventDefault();
      const results = this.fieldNames.map(name => this.validate(name));
      const allValid = results.every(Boolean);

      if (allValid) {
        this.form.dispatchEvent(new CustomEvent("formValid"));
      } else {
        const firstInvalid = this.fieldNames.find(name => !this.validate(name));
        if (firstInvalid) {
          this.form.querySelector(`[name="${firstInvalid}"]`)?.focus();
        }
      }
    });
  }
}

// Usage
const validator = new FormValidator("signup-form", {
  name(value) {
    if (!value.trim()) return "Name is required.";
    if (value.trim().length < 2) return "Name must be at least 2 characters.";
    return null;
  },
  email(value) {
    if (!value.trim()) return "Email is required.";
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return "Enter a valid email.";
    return null;
  },
  password(value) {
    if (!value) return "Password is required.";
    if (value.length < 8) return "Password must be at least 8 characters.";
    return null;
  },
});

document.getElementById("signup-form").addEventListener("formValid", () => {
  console.log("All fields valid — submit to server.");
});

The class fires a formValid custom event when all fields pass. Your application listens for that event and handles the actual submission however it needs to.

Accessibility Considerations

A validated form that screen reader users cannot navigate is not production-ready. A few additions make the difference.

Use aria-describedby to associate error messages with their inputs:


<input type="text" id="name" aria-describedby="name-error" />
<span class="error" id="name-error" role="alert"></span>

The role="alert" attribute causes screen readers to announce the error message automatically when it appears. The aria-describedby association means screen readers also read the error when the field is focused.

Add aria-invalid to invalid fields:


function showError(input, errorEl, message) {
  input.classList.add("invalid");
  input.setAttribute("aria-invalid", "true");
  errorEl.textContent = message;
}

function showValid(input, errorEl) {
  input.classList.remove("invalid");
  input.removeAttribute("aria-invalid");
  errorEl.textContent = "";
}

aria-invalid="true" signals to assistive technology that the field contains an error, which some screen readers announce directly.

Common Validation Patterns

A few rules you will reach for repeatedly:


// Required field
if (!value.trim()) return "This field is required.";

// Minimum length
if (value.trim().length < 3) return "Must be at least 3 characters.";

// Maximum length
if (value.trim().length > 100) return "Must be 100 characters or fewer.";

// Numbers only
if (!/^\d+$/.test(value)) return "Please enter numbers only.";

// URL format
if (!/^https?:\/\/.+\..+/.test(value)) return "Please enter a valid URL.";

// Checkbox must be checked
if (!checkbox.checked) return "You must accept the terms.";

// Date is in the future
if (new Date(value) <= new Date()) return "Date must be in the future.";

Tips for Form Validation in JavaScript

Validate on blur, not on every keystroke. Showing errors while the user is mid-word is frustrating. Wait until they leave the field.

Clear errors as soon as input becomes valid. Do not make users submit again to find out whether their correction worked.

Always validate on submit too. The user might never blur a field — they might tab straight to the submit button. The submit handler catches everything the blur listeners missed.

Never rely on client-side validation alone. Any value submitted from a browser can be altered before it arrives at your server. Validate again server-side before processing or storing anything.

Show one error per field at a time. Return the most important error first. Once the user fixes it, show the next one if needed. A field with three simultaneous error messages is overwhelming…

Form validation with plain JavaScript follows a consistent pattern: a validator function for each field, a helper that runs the validator and updates the UI, blur listeners for real-time feedback, and a submit handler that validates everything at once.

No library required. No dependency to update. Just a set of functions that do exactly what they say, wired together in a way that is easy to extend as your form grows.

Build the validators first. Then wire up the listeners. The rest is just connecting the pieces.

Exit mobile version