How to Implement Form Validation with Real-Time Feedback in Vanilla JavaScript

You can implement real-time form validation in vanilla JavaScript by attaching input event listeners to form fields that invoke validator functions, which toggle CSS classes on parent wrappers to show success or error states without requiring external libraries.

The form-validation project in the jisan-mia/dom-projects repository demonstrates a clean, component-based approach to validating registration forms using only HTML, CSS, and plain JavaScript. This implementation uses a declarative pattern where markup defines validation targets, CSS handles visual feedback, and JavaScript manages state logic—making it easy to extend with real-time feedback.

Architecture Overview

The solution separates concerns into three distinct layers. In projects/form-validation/index.html, each input lives inside a .form-group wrapper containing Font Awesome icons and a <small> element for error text. The stylesheet in projects/form-validation/style.css defines visual states through modifier classes (.success and .error). Finally, projects/form-validation/script.js contains the validation logic that bridges markup and styling.

This architecture allows you to add real-time validation by simply attaching event listeners that reuse the existing helper functions—no changes to HTML or CSS are required.

HTML Structure for Validation States

The markup uses semantic wrappers that decouple the input from its feedback elements. Each .form-group contains the input field, two icons for visual feedback, and a <small> tag for error messages.

<div class="form-group">
  <label>Username</label>
  <input type="text" id="username" autocomplete="off" />
  <i class="fas fa-check-circle"></i>
  <i class="fas fa-exclamation-circle"></i>
  <small>Error Msg</small>
</div>

(source: [projects/form-validation/index.html](https://github.com/jisan-mia/dom-projects/blob/main/projects/form-validation/index.html))

This structure enables the JavaScript to control the entire validation state by toggling a single class on the parent wrapper.

CSS Visual Feedback

The stylesheet defines the appearance of success and error states using the .form-group.success and .form-group.error selectors. When the parent wrapper has the success class, the green checkmark icon becomes visible; when it has the error class, the red exclamation icon and error text appear.

.form-group.success i.fa-check-circle {
  opacity: 1;
  color: #2ecc71;
}

.form-group.error i.fa-exclamation-circle {
  opacity: 1;
  color: #e74c3c;
}

.form-group small {
  color: #e74c3c;
  visibility: hidden;
}

.form-group.error small {
  visibility: visible;
}

(source: [projects/form-validation/style.css](https://github.com/jisan-mia/dom-projects/blob/main/projects/form-validation/style.css))

Because the script only manipulates the wrapper class, the visual feedback remains purely declarative and easy to customize.

JavaScript Validation Logic

The projects/form-validation/script.js file implements a separation-of-concerns pattern with utility validators, state helpers, and event wiring.

Core Helper Functions

Two functions handle all DOM state changes. setErrorMsg() adds the error class and injects the message text, while setSuccessmsg() adds the success class and clears any previous error.

const setErrorMsg = (input, msg) => {
  const group = input.parentNode;
  group.className = 'form-group error';
  group.querySelector('small').innerText = msg;
};

const setSuccessmsg = (input) => {
  const group = input.parentNode;
  group.className = 'form-group success';
};

Field-Specific Validators

Each input has a dedicated validator function that checks business rules and invokes the appropriate helper. For example, validateUserName() checks for empty values and minimum length, while validateEmail() uses a regex pattern via the isEmail() utility.

const isEmail = (email) => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};

const validateUserName = () => {
  const value = username.value.trim();
  if (!value) return setErrorMsg(username, 'Username cannot be blank');
  if (value.length < 3) return setErrorMsg(username, 'Enter at least 3 characters');
  setSuccessmsg(username);
};

const validateEmail = () => {
  const value = email.value.trim();
  if (!value) return setErrorMsg(email, 'Email cannot be blank');
  if (!isEmail(value)) return setErrorMsg(email, 'Invalid email address');
  setSuccessmsg(email);
};

Wiring Real-Time Events

To enable real-time feedback, attach input event listeners to each field that invoke their respective validators on every keystroke. This reuses the existing helper functions without requiring additional UI code.

username.addEventListener('input', validateUserName);
email.addEventListener('input', validateEmail);
password.addEventListener('input', validatePassword);
cpassword.addEventListener('input', validateConfirmPassword);

The submit handler performs a final validation sweep and checks that all groups have the success class before proceeding.

form.addEventListener('submit', (e) => {
  e.preventDefault();
  validateUserName(); 
  validateEmail(); 
  validatePassword(); 
  validateConfirmPassword();

  const allValid = [...document.querySelectorAll('.form-group')]
                    .every(g => g.classList.contains('success'));

  if (allValid) showSubmittedMsg();
});

(source: [projects/form-validation/script.js](https://github.com/jisan-mia/dom-projects/blob/main/projects/form-validation/script.js))

Complete Working Example

Here is the minimal implementation combining all three layers. This example validates a username and email field with real-time feedback as implemented in the DOM-Projects repository.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Real-Time Validation</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
  <style>
    .form-group { margin-bottom: 1rem; position: relative; }
    .form-group i { position: absolute; right: 10px; top: 35px; opacity: 0; }
    .form-group.success i.fa-check-circle { opacity: 1; color: #2ecc71; }
    .form-group.error i.fa-exclamation-circle { opacity: 1; color: #e74c3c; }
    .form-group small { color: #e74c3c; visibility: hidden; display: block; margin-top: 5px; }
    .form-group.error small { visibility: visible; }
  </style>
</head>
<body>
  <form id="form">
    <div class="form-group">
      <label>Username</label>
      <input type="text" id="username" autocomplete="off">
      <i class="fas fa-check-circle"></i>
      <i class="fas fa-exclamation-circle"></i>
      <small>Error message</small>
    </div>
    <button type="submit">Submit</button>
  </form>

  <script>
    const form = document.getElementById('form');
    const username = document.getElementById('username');

    const setErrorMsg = (input, msg) => {
      const group = input.parentNode;
      group.className = 'form-group error';
      group.querySelector('small').innerText = msg;
    };

    const setSuccessmsg = (input) => {
      const group = input.parentNode;
      group.className = 'form-group success';
    };

    const validateUserName = () => {
      const value = username.value.trim();
      if (!value) return setErrorMsg(username, 'Username cannot be blank');
      if (value.length < 3) return setErrorMsg(username, 'Enter at least 3 characters');
      setSuccessmsg(username);
    };

    username.addEventListener('input', validateUserName);

    form.addEventListener('submit', (e) => {
      e.preventDefault();
      validateUserName();
      const isValid = username.parentNode.classList.contains('success');
      if (isValid) alert('Form submitted successfully!');
    });
  </script>
</body>
</html>

Summary

  • Structure: Wrap each input in a .form-group container with icons and error text to allow class-based state toggling.
  • Visual Feedback: Use CSS modifiers (.success and .error) to control icon visibility and error message display without inline styles.
  • Validation Logic: Implement reusable helpers like setErrorMsg() and setSuccessmsg() that manipulate the parent wrapper class to trigger CSS changes.
  • Real-Time Updates: Attach input event listeners to each field that invoke field-specific validators on every keystroke, providing immediate feedback as implemented in projects/form-validation/script.js.
  • Final Check: Always validate all fields on submit to catch untouched fields before allowing form submission.

Frequently Asked Questions

What is the difference between using input and blur events for real-time validation?

The input event fires immediately whenever the value of an element changes, providing instant feedback as the user types. The blur event only fires when the user leaves the field, which reduces noise during typing but delays feedback until the user moves to the next field. For the most responsive experience as shown in the DOM-Projects implementation, use input events on each field.

Can I implement this validation pattern without Font Awesome icons?

Yes, the validation logic is completely independent of the icon library. In projects/form-validation/style.css, the icons are controlled by opacity changes based on the parent class. You can replace the <i> tags with SVG elements, emoji characters, or simply change border colors and background images—the JavaScript only toggles the .success or .error class on the .form-group wrapper.

How do I add asynchronous validation, such as checking username availability?

Extend the field-specific validator to return a Promise and use async/await inside the event listener. For example, modify validateUserName() to fetch from an API endpoint before calling setSuccessmsg(). To prevent excessive network requests, debounce the input event using setTimeout or AbortController to cancel pending requests. The existing setErrorMsg() and setSuccessmsg() helpers work identically for async validation since they only manipulate DOM classes.

Is this validation approach accessible for screen readers?

The current implementation provides visual feedback through icons and color changes, but you should enhance accessibility by adding ARIA attributes dynamically. When setErrorMsg() is called, set input.setAttribute('aria-invalid', 'true') and input.setAttribute('aria-describedby', 'error-id') pointing to the <small> element. For setSuccessmsg(), set aria-invalid to false and remove the describedby attribute. This ensures screen readers announce validation states as users type.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →