Skip to main content

📝 Lesson 17: Forms & Validation

Forms are how users send data to your application — login credentials, search queries, registration info, feedback. In this lesson you'll learn to read form values with JavaScript, intercept submissions, and validate input before it goes anywhere.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Access form elements and read their values with JavaScript
  • Handle the submit event and use preventDefault()
  • Build validation rules for required fields, length, format, and matching
  • Provide real-time validation feedback on input and blur events
  • Display custom error messages and style valid/invalid fields
  • Use the Constraint Validation API for built-in browser validation
  • Combine everything into a complete, accessible validation system

Estimated Time: 55 minutes

Project: Build a registration form with real-time validation

📑 In This Lesson

Forms in HTML — A Quick Refresher

Before we dive into JavaScript, let's make sure the HTML side is solid. A <form> groups related inputs together and defines how and where data is submitted.


<form id="signup-form" action="/api/signup" method="POST">
    <label for="username">Username</label>
    <input type="text" id="username" name="username" required>

    <label for="email">Email</label>
    <input type="email" id="email" name="email" required>

    <label for="password">Password</label>
    <input type="password" id="password" name="password" required>

    <button type="submit">Sign Up</button>
</form>
                

Common Input Types

HTML provides many built-in input types, each with its own keyboard behavior and basic validation.

Type Purpose Example
textGeneral single-line textName, username
emailEmail address (validates format)user@example.com
passwordHidden text inputLogin password
numberNumeric value with spinnersAge, quantity
telPhone number (mobile keyboard)555-1234
urlURL (validates format)https://example.com
checkboxOn/off toggleAgree to terms
radioOne choice from a groupPlan selection
textareaMulti-line text (not an input!)Comments, bio
selectDropdown menu (not an input!)Country, category

💡 Labels Matter

Always pair each input with a <label> using the for attribute. This isn't just a best practice — it's essential for accessibility. Screen readers use labels to describe inputs, and clicking a label focuses its associated input, making forms easier to use for everyone.

Reading Form Values with JavaScript

Every form element has a .value property that gives you whatever the user has typed or selected. This is how JavaScript reads form data.

Accessing Individual Inputs


// By ID (most common)
const username = document.querySelector("#username");
console.log(username.value);  // Whatever the user typed

// The value is always a string — even for number inputs
const age = document.querySelector("#age");
console.log(age.value);       // "25" (string!)
console.log(Number(age.value)); // 25 (number)
                

Different Input Types, Different Properties


// Text, email, password, number, tel, url → use .value
const email = document.querySelector("#email");
console.log(email.value);  // "user@example.com"

// Checkbox → use .checked (boolean)
const terms = document.querySelector("#agree-terms");
console.log(terms.checked);  // true or false

// Radio buttons → find the checked one
const plan = document.querySelector('input[name="plan"]:checked');
console.log(plan?.value);  // "premium" (or undefined if none selected)

// Select dropdown → use .value
const country = document.querySelector("#country");
console.log(country.value);  // "US"

// Textarea → use .value (same as text inputs)
const bio = document.querySelector("#bio");
console.log(bio.value);  // Multi-line text string
                

Accessing Inputs Through the Form

You can also access inputs through the form element itself using the name attribute. This avoids needing individual IDs for every field.


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

// Access by name attribute
console.log(form.elements.username.value);
console.log(form.elements.email.value);

// Or use bracket notation (useful for names with hyphens)
console.log(form.elements["first-name"].value);

// form.elements is an HTMLFormControlsCollection — it's live
console.log(form.elements.length);  // Number of form controls
                
graph LR A["<form>"] --> B["form.elements"] B --> C["By name
.username"] B --> D["By index
[0]"] B --> E["By ID
.email"] C --> F[".value"] D --> F E --> F style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style C fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style D fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style E fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style F fill:#fce7f3,stroke:#ec4899,stroke-width:2px,color:#1e293b

Handling Form Submission

By default, submitting a form reloads the page (or navigates to the action URL). In modern JavaScript, we almost always want to prevent that default behavior and handle the data ourselves.

The submit Event & preventDefault()


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

form.addEventListener("submit", (event) => {
    // Stop the browser from reloading the page
    event.preventDefault();

    // Now we can handle the data ourselves
    const username = form.elements.username.value;
    const email = form.elements.email.value;

    console.log("Submitted:", { username, email });

    // Later: validate, then send to server via fetch()
});
                

⚠️ Why preventDefault() Is Essential

Without event.preventDefault(), the browser will navigate away from your page when the form is submitted. Your JavaScript code after the submit event won't have time to run — the page is already reloading. Always call it first in your submit handler.

Gathering All Form Data at Once

The FormData API gives you a convenient way to collect all form values without reading each input individually.


form.addEventListener("submit", (event) => {
    event.preventDefault();

    const formData = new FormData(form);

    // Get individual values
    console.log(formData.get("username"));
    console.log(formData.get("email"));

    // Convert to a plain object
    const data = Object.fromEntries(formData);
    console.log(data);
    // { username: "ray", email: "ray@example.com", password: "..." }

    // Loop through all entries
    for (const [name, value] of formData) {
        console.log(`${name}: ${value}`);
    }
});
                

Resetting the Form


// After successful submission, clear the form
form.reset();  // Resets all fields to their initial values

// Or reset individual fields
form.elements.username.value = "";
                
graph TD A["User clicks Submit"] --> B["submit event fires"] B --> C["preventDefault()"] C --> D["Read form values"] D --> E{"Validate data"} E -->|Valid| F["Send to server
(fetch)"] E -->|Invalid| G["Show error
messages"] F --> H["Show success &
reset form"] G --> I["User fixes
errors"] I --> A style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style C fill:#fce7f3,stroke:#ec4899,stroke-width:2px,color:#1e293b style D fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style E fill:#f3e8ff,stroke:#a855f7,stroke-width:2px,color:#1e293b style F fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style G fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b style H fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style I fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b

Validation Patterns

Client-side validation gives users immediate feedback — no waiting for a server round-trip. Here are the most common validation checks you'll build.

Required Fields


function isRequired(value) {
    return value.trim() !== "";
}

// Usage
if (!isRequired(username.value)) {
    showError(username, "Username is required");
}
                

Length Validation


function isLengthValid(value, min, max) {
    const length = value.trim().length;
    return length >= min && length <= max;
}

// Username: 3–20 characters
if (!isLengthValid(username.value, 3, 20)) {
    showError(username, "Username must be 3–20 characters");
}

// Password: at least 8 characters
if (password.value.length < 8) {
    showError(password, "Password must be at least 8 characters");
}
                

Pattern Matching with Regular Expressions


// Email: basic format check
function isValidEmail(value) {
    // Checks for something@something.something
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}

// Username: letters, numbers, underscores only
function isValidUsername(value) {
    return /^[a-zA-Z0-9_]+$/.test(value);
}

// Password: at least one uppercase, one lowercase, one digit
function isStrongPassword(value) {
    const hasUpper = /[A-Z]/.test(value);
    const hasLower = /[a-z]/.test(value);
    const hasDigit = /\d/.test(value);
    return hasUpper && hasLower && hasDigit && value.length >= 8;
}
                

Matching Fields


// Confirm password matches
function doFieldsMatch(value1, value2) {
    return value1 === value2;
}

const password = form.elements.password.value;
const confirm = form.elements["confirm-password"].value;

if (!doFieldsMatch(password, confirm)) {
    showError(confirmInput, "Passwords do not match");
}
                

Checkbox Validation


const terms = document.querySelector("#agree-terms");

if (!terms.checked) {
    showError(terms, "You must agree to the terms");
}
                

Putting It Together: A Validate Function


function validateForm(form) {
    let isValid = true;

    // Clear previous errors
    clearErrors(form);

    const username = form.elements.username.value;
    const email = form.elements.email.value;
    const password = form.elements.password.value;

    if (!isRequired(username)) {
        showError(form.elements.username, "Username is required");
        isValid = false;
    } else if (!isLengthValid(username, 3, 20)) {
        showError(form.elements.username, "Username must be 3–20 characters");
        isValid = false;
    }

    if (!isRequired(email)) {
        showError(form.elements.email, "Email is required");
        isValid = false;
    } else if (!isValidEmail(email)) {
        showError(form.elements.email, "Please enter a valid email");
        isValid = false;
    }

    if (!isRequired(password)) {
        showError(form.elements.password, "Password is required");
        isValid = false;
    } else if (!isStrongPassword(password)) {
        showError(form.elements.password, "Use 8+ chars with uppercase, lowercase, and a number");
        isValid = false;
    }

    return isValid;
}

form.addEventListener("submit", (event) => {
    event.preventDefault();
    if (validateForm(form)) {
        console.log("Form is valid — submit to server!");
    }
});
                

⚠️ Client-Side Validation Is Not Enough

Client-side validation is for user experience — it gives instant feedback and prevents obvious mistakes. But it can always be bypassed (DevTools, disabled JavaScript, direct API calls). Always validate on the server too. Think of client-side validation as a helpful first line of defense, not a security boundary.

Real-Time Validation

Validating only on submit is the bare minimum. A better experience gives feedback as the user types or tabs away from a field.

Choosing the Right Event

Event When It Fires Best For
inputEvery keystroke / changeLive character counts, password strength
blurUser leaves the fieldFull field validation (email, username)
focusUser enters the fieldShowing hints or clearing errors
changeValue changes & field loses focusSelects, checkboxes, radio buttons

Validate on blur, Feedback on input

A common pattern: validate when the user leaves a field (blur) and provide live feedback as they type (input), but only after the first blur. This avoids showing errors before the user has even finished typing.


// Track which fields have been "touched" (blurred at least once)
const touched = new Set();

function setupField(input, validateFn) {
    // Validate when the user leaves the field
    input.addEventListener("blur", () => {
        touched.add(input.name);
        validateFn(input);
    });

    // Re-validate on input, but only if already touched
    input.addEventListener("input", () => {
        if (touched.has(input.name)) {
            validateFn(input);
        }
    });
}

// Validate the email field
function validateEmail(input) {
    const value = input.value.trim();
    if (!value) {
        showError(input, "Email is required");
    } else if (!isValidEmail(value)) {
        showError(input, "Please enter a valid email");
    } else {
        showSuccess(input);
    }
}

setupField(form.elements.email, validateEmail);
                

Showing and Clearing Errors


function showError(input, message) {
    // Remove any existing success state
    input.classList.remove("input-success");
    input.classList.add("input-error");

    // Find or create the error message element
    let error = input.parentElement.querySelector(".error-message");
    if (!error) {
        error = document.createElement("span");
        error.classList.add("error-message");
        error.setAttribute("role", "alert");  // Accessibility!
        input.parentElement.append(error);
    }
    error.textContent = message;
}

function showSuccess(input) {
    input.classList.remove("input-error");
    input.classList.add("input-success");

    const error = input.parentElement.querySelector(".error-message");
    if (error) error.remove();
}

function clearErrors(form) {
    form.querySelectorAll(".error-message").forEach(el => el.remove());
    form.querySelectorAll(".input-error, .input-success").forEach(el => {
        el.classList.remove("input-error", "input-success");
    });
}
                

Styling Valid & Invalid States


<style>
    .input-error {
        border-color: #ef4444;
        background-color: #fef2f2;
    }
    .input-success {
        border-color: #22c55e;
        background-color: #f0fdf4;
    }
    .error-message {
        color: #ef4444;
        font-size: 0.85rem;
        margin-top: 0.25rem;
        display: block;
    }
</style>
                

✅ Accessibility Tips for Validation

  • Use role="alert" on error messages so screen readers announce them immediately
  • Associate errors with inputs using aria-describedby pointing to the error element's ID
  • Don't rely only on color — add text messages and/or icons
  • Focus the first invalid field after a failed submission attempt
graph LR A["User types"] --> B{"Field
touched?"} B -->|No| C["No feedback yet"] B -->|Yes| D["Validate on input"] D -->|Valid| E["✅ Green border"] D -->|Invalid| F["❌ Red border
+ error message"] G["User leaves field
(blur)"] --> H["Mark as touched"] H --> D style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style B fill:#f3e8ff,stroke:#a855f7,stroke-width:2px,color:#1e293b style C fill:#f8fafc,stroke:#94a3b8,stroke-width:2px,color:#1e293b style D fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b style E fill:#ecfdf5,stroke:#22c55e,stroke-width:2px,color:#1e293b style F fill:#fef2f2,stroke:#ef4444,stroke-width:2px,color:#1e293b style G fill:#eff6ff,stroke:#3b82f6,stroke-width:2px,color:#1e293b style H fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#1e293b

The Constraint Validation API

The browser has a built-in validation system called the Constraint Validation API. When you use HTML attributes like required, minlength, pattern, and type="email", the browser validates automatically and provides its own error messages. JavaScript can hook into this system.

HTML Validation Attributes


<input type="text" required minlength="3" maxlength="20"
       pattern="[a-zA-Z0-9_]+"
       title="Letters, numbers, and underscores only">

<input type="email" required>

<input type="number" min="1" max="120" step="1">

<input type="url" required>
                

Checking Validity in JavaScript


const input = document.querySelector("#email");

// Check if the input is valid
console.log(input.validity.valid);         // true or false

// Check specific validity states
console.log(input.validity.valueMissing);  // true if required & empty
console.log(input.validity.typeMismatch);  // true if wrong type (e.g. not email)
console.log(input.validity.tooShort);      // true if below minlength
console.log(input.validity.tooLong);       // true if above maxlength
console.log(input.validity.patternMismatch); // true if doesn't match pattern

// Check the entire form at once
const form = document.querySelector("#signup-form");
console.log(form.checkValidity());  // true if ALL fields valid
                

Custom Validation Messages


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

email.addEventListener("invalid", (event) => {
    // Prevent the browser's default tooltip
    event.preventDefault();

    // Show your own message
    if (email.validity.valueMissing) {
        showError(email, "We need your email to create your account");
    } else if (email.validity.typeMismatch) {
        showError(email, "That doesn't look like an email address");
    }
});

// Or set a custom message for the browser's built-in tooltip
email.setCustomValidity("Please enter your company email");
// IMPORTANT: Clear it when valid, or the field stays "invalid"
email.addEventListener("input", () => {
    email.setCustomValidity("");
});
                

Disabling Built-In Validation

If you want full control with your own custom validation, disable the browser's built-in tooltips with novalidate.


<!-- The browser won't show its own error messages -->
<form id="signup-form" novalidate>
    <!-- HTML attributes still work with the Constraint API -->
    <input type="email" required>
    <button type="submit">Submit</button>
</form>
                

// You can still use checkValidity() and validity states
// but now YOU control what error messages look like
form.addEventListener("submit", (event) => {
    event.preventDefault();

    if (!form.checkValidity()) {
        // Show your own custom errors
        highlightInvalidFields(form);
    } else {
        // Form is valid — proceed
        submitData(form);
    }
});
                

💡 Which Approach Should You Use?

For simple forms (contact, newsletter signup), the built-in browser validation with a few HTML attributes might be all you need. For complex forms with custom rules (password strength, field matching, conditional fields), use novalidate and build your own validation — but you can still use the Constraint Validation API's validity object to check individual states.

Hands-on Exercise

🏋️ Exercise: Registration Form with Real-Time Validation

Objective: Build a registration form that validates username, email, password, and password confirmation in real time. Fields validate on blur, then re-validate on input. The form only submits when all fields are valid.


<!-- Save this as registration.html and open in a browser -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Registration Form</title>
    <style>
        * { box-sizing: border-box; margin: 0; }
        body { font-family: system-ui, sans-serif; background: #f1f5f9; padding: 2rem; }
        .form-container {
            max-width: 450px; margin: 0 auto; background: white;
            padding: 2rem; border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.08);
        }
        h1 { margin-bottom: 1.5rem; color: #1e293b; }
        .form-group { margin-bottom: 1.25rem; }
        label {
            display: block; font-weight: 600; margin-bottom: 0.35rem;
            color: #334155; font-size: 0.9rem;
        }
        input {
            width: 100%; padding: 0.6rem 0.75rem; border: 2px solid #cbd5e1;
            border-radius: 8px; font-size: 1rem; transition: border-color 0.2s;
        }
        input:focus { outline: none; border-color: #6366f1; }
        input.input-error { border-color: #ef4444; background: #fef2f2; }
        input.input-success { border-color: #22c55e; background: #f0fdf4; }
        .error-message {
            color: #ef4444; font-size: 0.8rem; margin-top: 0.3rem; display: block;
        }
        .strength-meter {
            height: 4px; border-radius: 2px; margin-top: 0.4rem;
            background: #e2e8f0; overflow: hidden;
        }
        .strength-bar {
            height: 100%; width: 0; border-radius: 2px;
            transition: width 0.3s, background-color 0.3s;
        }
        .strength-text { font-size: 0.8rem; margin-top: 0.2rem; color: #64748b; }
        button[type="submit"] {
            width: 100%; padding: 0.75rem; background: #6366f1; color: white;
            border: none; border-radius: 8px; font-size: 1rem;
            cursor: pointer; margin-top: 0.5rem; font-weight: 600;
        }
        button[type="submit"]:hover { background: #4f46e5; }
        button[type="submit"]:disabled { background: #94a3b8; cursor: not-allowed; }
        .success-message {
            text-align: center; color: #22c55e; font-weight: 600;
            display: none; padding: 1rem;
        }
    </style>
</head>
<body>
    <div class="form-container">
        <h1>📋 Create an Account</h1>
        <form id="register-form" novalidate>
            <div class="form-group">
                <label for="username">Username</label>
                <input type="text" id="username" name="username"
                       placeholder="3–20 characters, letters/numbers/underscores">
            </div>
            <div class="form-group">
                <label for="email">Email</label>
                <input type="email" id="email" name="email"
                       placeholder="you@example.com">
            </div>
            <div class="form-group">
                <label for="password">Password</label>
                <input type="password" id="password" name="password"
                       placeholder="At least 8 characters">
                <div class="strength-meter">
                    <div class="strength-bar" id="strength-bar"></div>
                </div>
                <span class="strength-text" id="strength-text"></span>
            </div>
            <div class="form-group">
                <label for="confirm-password">Confirm Password</label>
                <input type="password" id="confirm-password" name="confirm-password"
                       placeholder="Re-enter your password">
            </div>
            <button type="submit">Create Account</button>
        </form>
        <div class="success-message" id="success-message">
            ✅ Account created successfully!
        </div>
    </div>

    <script>
    // TODO: Complete each task

    // 1. Write helper functions:
    //    - showError(input, message) — adds "input-error" class
    //      and shows a .error-message span
    //    - showSuccess(input) — adds "input-success" class
    //      and removes any .error-message
    //    - clearErrors(form) — removes all error states

    // 2. Write validation functions for each field:
    //    - validateUsername: required, 3–20 chars, letters/numbers/underscores
    //    - validateEmail: required, valid email format
    //    - validatePassword: required, 8+ chars, uppercase + lowercase + digit
    //    - validateConfirm: required, matches password

    // 3. Write a password strength meter:
    //    - On "input" event on the password field
    //    - Score: +1 for length >= 8, +1 for uppercase, +1 lowercase,
    //      +1 digit, +1 special char
    //    - Update the width and color of #strength-bar
    //    - Update the text in #strength-text

    // 4. Set up real-time validation:
    //    - Track "touched" fields (blur marks a field as touched)
    //    - On blur: validate the field
    //    - On input: re-validate only if already touched

    // 5. Handle form submission:
    //    - preventDefault
    //    - Run all validations
    //    - If all valid: hide the form, show #success-message
    //    - If invalid: focus the first invalid field
    </script>
</body>
</html>
                    
💡 Hint

1: For showError, find an existing .error-message in the input's parent or create one with createElement("span"). Add role="alert" for accessibility.

2: Each validator takes an input element, checks input.value.trim(), calls showError or showSuccess, and returns true or false.

3: Use separate regex tests: /[A-Z]/, /[a-z]/, /\d/, /[^a-zA-Z0-9]/. Map score 0–1 to red, 2–3 to orange/yellow, 4–5 to green.

4: Use a Set to track touched field names. On blur, add to the set and validate. On input, check if the name is in the set before validating.

5: Call all four validators. If any returns false, find the first .input-error and call .focus() on it.

✅ Solution

const form = document.querySelector("#register-form");
const strengthBar = document.querySelector("#strength-bar");
const strengthText = document.querySelector("#strength-text");
const successMessage = document.querySelector("#success-message");
const touched = new Set();

// 1. Helper functions
function showError(input, message) {
    input.classList.remove("input-success");
    input.classList.add("input-error");

    let error = input.parentElement.querySelector(".error-message");
    if (!error) {
        error = document.createElement("span");
        error.classList.add("error-message");
        error.setAttribute("role", "alert");
        input.after(error);
    }
    error.textContent = message;
}

function showSuccess(input) {
    input.classList.remove("input-error");
    input.classList.add("input-success");
    const error = input.parentElement.querySelector(".error-message");
    if (error) error.remove();
}

function clearErrors(form) {
    form.querySelectorAll(".error-message").forEach(el => el.remove());
    form.querySelectorAll(".input-error, .input-success").forEach(el => {
        el.classList.remove("input-error", "input-success");
    });
}

// 2. Validation functions
function validateUsername(input) {
    const value = input.value.trim();
    if (!value) {
        showError(input, "Username is required");
        return false;
    }
    if (value.length < 3 || value.length > 20) {
        showError(input, "Username must be 3–20 characters");
        return false;
    }
    if (!/^[a-zA-Z0-9_]+$/.test(value)) {
        showError(input, "Only letters, numbers, and underscores");
        return false;
    }
    showSuccess(input);
    return true;
}

function validateEmail(input) {
    const value = input.value.trim();
    if (!value) {
        showError(input, "Email is required");
        return false;
    }
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
        showError(input, "Please enter a valid email address");
        return false;
    }
    showSuccess(input);
    return true;
}

function validatePassword(input) {
    const value = input.value;
    if (!value) {
        showError(input, "Password is required");
        return false;
    }
    if (value.length < 8) {
        showError(input, "Password must be at least 8 characters");
        return false;
    }
    if (!/[A-Z]/.test(value) || !/[a-z]/.test(value) || !/\d/.test(value)) {
        showError(input, "Include uppercase, lowercase, and a number");
        return false;
    }
    showSuccess(input);
    return true;
}

function validateConfirm(input) {
    const value = input.value;
    const password = form.elements.password.value;
    if (!value) {
        showError(input, "Please confirm your password");
        return false;
    }
    if (value !== password) {
        showError(input, "Passwords do not match");
        return false;
    }
    showSuccess(input);
    return true;
}

// 3. Password strength meter
form.elements.password.addEventListener("input", () => {
    const value = form.elements.password.value;
    let score = 0;

    if (value.length >= 8) score++;
    if (/[A-Z]/.test(value)) score++;
    if (/[a-z]/.test(value)) score++;
    if (/\d/.test(value)) score++;
    if (/[^a-zA-Z0-9]/.test(value)) score++;

    const percent = (score / 5) * 100;
    strengthBar.style.width = percent + "%";

    const levels = [
        { label: "", color: "#e2e8f0" },
        { label: "Very weak", color: "#ef4444" },
        { label: "Weak", color: "#f97316" },
        { label: "Fair", color: "#eab308" },
        { label: "Strong", color: "#22c55e" },
        { label: "Very strong", color: "#16a34a" }
    ];

    strengthBar.style.backgroundColor = levels[score].color;
    strengthText.textContent = value ? levels[score].label : "";
    strengthText.style.color = levels[score].color;
});

// 4. Real-time validation
const validators = {
    username: validateUsername,
    email: validateEmail,
    password: validatePassword,
    "confirm-password": validateConfirm
};

Object.keys(validators).forEach(name => {
    const input = form.elements[name];

    input.addEventListener("blur", () => {
        touched.add(name);
        validators[name](input);
    });

    input.addEventListener("input", () => {
        if (touched.has(name)) {
            validators[name](input);
        }
    });
});

// 5. Handle submission
form.addEventListener("submit", (event) => {
    event.preventDefault();

    // Mark all fields as touched
    Object.keys(validators).forEach(name => touched.add(name));

    const results = Object.entries(validators).map(
        ([name, fn]) => fn(form.elements[name])
    );

    if (results.every(Boolean)) {
        form.style.display = "none";
        successMessage.style.display = "block";
    } else {
        const firstInvalid = form.querySelector(".input-error");
        if (firstInvalid) firstInvalid.focus();
    }
});
                        

🎯 Quick Quiz

Question 1: How do you read what the user typed into a text input?

Question 2: What does event.preventDefault() do in a submit handler?

Question 3: How do you check if a checkbox is checked?

Question 4: When is the blur event useful for form validation?

Question 5: Why is client-side validation not sufficient on its own?

Summary

🎉 Key Takeaways

  • Read input values with .value (text, email, password) or .checked (checkboxes, radios)
  • Access inputs via form.elements.name for clean, name-based access
  • Always call event.preventDefault() in your submit handler to stop page reloads
  • Use FormData to collect all form values at once
  • Validate fields for required, length, format (regex), and matching
  • Validate on blur (when user leaves), then re-validate on input (as they type) — but only after the field has been touched
  • Show errors with role="alert" for accessibility; style with color + text, not color alone
  • The Constraint Validation API lets you hook into the browser's built-in validation system
  • Client-side validation is for UX; always validate on the server too

📚 Additional Resources

🚀 What's Next?

Now that you can capture and validate user input, it's time to add motion and timing to your pages. In the next lesson, you'll learn to use setTimeout, setInterval, and requestAnimationFrame to create timers, countdowns, and smooth animations driven by JavaScript.