Skip to content

JavaScript Tutorial Practical Projects

Project 7: A Validated Signup Form

Lesson 246 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

This project puts the forms chapter together: field rules, helpful messages, accessible errors and a result.

index.html
HTML
<style>
  .form { font-family: system-ui, sans-serif; max-width: 320px; }
  .field { margin-bottom: 12px; }
  label { display: block; font-weight: 600; margin-bottom: 4px; }
  input { width: 100%; box-sizing: border-box; padding: 8px; font: inherit; border: 2px solid #b9b2ad; border-radius: 6px; }
  input[aria-invalid="true"] { border-color: #b00020; }
  input:focus-visible { outline: 3px solid #1a527655; }
  .error { color: #b00020; font-size: 14px; min-height: 1.2em; margin: 2px 0 0; }
  .hint { color: #555; font-size: 13px; }
  button { font: inherit; padding: 8px 16px; background: #1a5276; color: #fff; border: 0; border-radius: 6px; cursor: pointer; }
  .strength { height: 6px; background: #eee; border-radius: 3px; margin-top: 4px; overflow: hidden; }
  .strength span { display: block; height: 100%; width: 0; background: #b00020; transition: width .2s, background .2s; }
</style>

<form class="form" id="form" novalidate>
  <div class="field">
    <label for="name">Name</label>
    <input id="name" name="name" autocomplete="name" aria-describedby="name-error">
    <p class="error" id="name-error"></p>
  </div>
  <div class="field">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" autocomplete="email" aria-describedby="email-error">
    <p class="error" id="email-error"></p>
  </div>
  <div class="field">
    <label for="password">Password</label>
    <input id="password" name="password" type="password" autocomplete="new-password" aria-describedby="pw-hint password-error">
    <div class="strength" aria-hidden="true"><span id="bar"></span></div>
    <p class="hint" id="pw-hint">At least 8 characters, with a letter and a number.</p>
    <p class="error" id="password-error"></p>
  </div>
  <button>Create account</button>
  <p role="status" id="result"></p>
</form>

<script>
  const rules = {
    name: v => (v.length < 2 ? "Please enter your name (at least 2 letters)." : ""),
    email: v => (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? "" : "Enter an email address like name@example.com."),
    password: v => {
      if (v.length < 8) return "Use at least 8 characters.";
      if (!/[a-zA-Z]/.test(v) || !/\d/.test(v)) return "Include at least one letter and one number.";
      return "";
    }
  };

  const form = document.querySelector("#form");

  function check(input) {
    const message = rules[input.name](input.value.trim());
    document.getElementById(input.name + "-error").textContent = message;
    input.setAttribute("aria-invalid", String(message !== ""));
    return message === "";
  }

  function strength(password) {
    let score = 0;
    if (password.length >= 8) score++;
    if (/[A-Z]/.test(password) && /[a-z]/.test(password)) score++;
    if (/\d/.test(password)) score++;
    if (/[^A-Za-z0-9]/.test(password)) score++;
    return score;                                    // 0 to 4
  }

  form.addEventListener("input", event => {
    if (event.target.name === "password") {
      const score = strength(event.target.value);
      const bar = document.querySelector("#bar");
      bar.style.width = score * 25 + "%";
      bar.style.background = score < 2 ? "#b00020" : score < 4 ? "#d98b00" : "#1e8449";
    }
    if (event.target.getAttribute("aria-invalid") === "true") check(event.target);   // re-check once an error was shown
  });

  form.addEventListener("focusout", event => {
    if (event.target.name in rules && event.target.value !== "") check(event.target);
  });

  form.addEventListener("submit", event => {
    event.preventDefault();
    const inputs = [...form.querySelectorAll("input")];
    const results = inputs.map(check);                // check every field
    const result = document.querySelector("#result");

    if (results.every(Boolean)) {
      result.textContent = `Welcome, ${form.elements.name.value.trim()}!`;
      form.reset();
    } else {
      result.textContent = "";
      inputs[results.indexOf(false)].focus();
    }
  });
</script>

The design decisions

  • One rules object maps each field name to a function that returns an error message (or an empty string). Adding a field means adding a rule.
  • When to validate: on focusout (after leaving a field), on submit (everything), and on input only after an error has appeared. People are not nagged while they type their first characters.
  • Accessible errors: text under each field, linked with aria-describedby, aria-invalid, and focus moved to the first invalid field.
  • Strength meter is decorative (aria-hidden). The real requirement is written in text.
  • novalidate hands control to our messages. See Custom Validation.

Security reminder

This is only the front end. A real server must validate everything again, store passwords with a proper hashing algorithm, and never trust the browser.

Try changing it

  1. Add a Confirm password field that must match.
  2. Show or hide the password with a toggle button (input.type = "text").
  3. Disable the submit button while “sending” and show a loading message.
  4. Check whether the email is “taken” with a pretend async function (Async/Await).