Live Form Validation (Advanced)

Full-featured live form validation for Webflow with support for text inputs, email, select dropdowns, checkboxes, radio buttons, and a custom submit handler. Includes anti-spam protection and dynamic validation state classes.

validationjavascriptwebflowcheckboxradioselectemail
Live Form Validation (Advanced) preview

Code

HTML
html
<div data-form-validate="" class="form-group w-form">
  <form id="wf-form-Default-Form" name="wf-form-Default-Form" data-name="Default Form" method="get" class="form" data-wf-page-id="6780d77ea8657fba8f267b71" data-wf-element-id="7310ef1a-7190-2f9a-9ac2-8af00a76fc82">
    <div data-validate="" class="form-field-group">
      <label for="name" class="form-label">Name <span class="form-required">*</span></label>
      <div class="form-field">
        <input class="form-input w-input" maxlength="256" name="name" data-name="Name" min="1" placeholder="Osmo" type="text" id="name" required="">
        <div class="form-field-icon is--success"><!-- success SVG icon --></div>
        <div class="form-field-icon is--error"><!-- error SVG icon --></div>
      </div>
    </div>
    <div data-validate="" class="form-field-group">
      <label for="email" class="form-label">Email Address <span class="form-required">*</span></label>
      <div class="form-field">
        <input class="form-input w-input" maxlength="256" name="email" data-name="Email" placeholder="hello@osmo.supply" type="email" id="email" required="">
        <div class="form-field-icon is--success"><!-- success SVG icon --></div>
        <div class="form-field-icon is--error"><!-- error SVG icon --></div>
      </div>
    </div>
    <div data-validate="" class="form-field-group">
      <label for="location" class="form-label">Location <span class="form-required">*</span> <span class="form-inactive-text">(select min. 2)</span></label>
      <div max="2" data-radiocheck-group="" min="2" class="radiocheck-group">
        <label class="w-checkbox radiocheck-field">
          <input type="checkbox" name="Location-Netherlands" id="Location-Netherlands" data-name="Location Netherlands" class="w-checkbox-input checkbox-input">
          <span class="radiocheck-label w-form-label" for="Location-Netherlands">The Netherlands</span>
          <div class="radiocheck-custom"><!-- check SVG --></div>
        </label>
        <label class="w-checkbox radiocheck-field">
          <input type="checkbox" name="Location-Germany" id="Location-Germany" data-name="Location Germany" class="w-checkbox-input checkbox-input">
          <span class="radiocheck-label w-form-label" for="Location-Germany">Germany</span>
          <div class="radiocheck-custom"><!-- check SVG --></div>
        </label>
        <label class="w-checkbox radiocheck-field">
          <input type="checkbox" name="Location-Belgium" id="Location-Belgium" data-name="Location Belgium" class="w-checkbox-input checkbox-input">
          <span class="radiocheck-label w-form-label" for="Location-Belgium">Belgium</span>
          <div class="radiocheck-custom"><!-- check SVG --></div>
        </label>
        <div class="radiocheck-field-icon is--success"><!-- success SVG icon --></div>
        <div class="radiocheck-field-icon is--error"><!-- error SVG icon --></div>
      </div>
    </div>
    <div data-validate="" class="form-field-group">
      <label for="category" class="form-label">Category <span class="form-required">*</span></label>
      <div class="form-field">
        <select id="field" name="field" data-name="Field" required="" class="form-input w-select">
          <option value="">Select option</option>
          <option value="First">First choice</option>
          <option value="Second">Second choice</option>
          <option value="Third">Third choice</option>
        </select>
        <div class="form-field-chevron"><!-- chevron SVG --></div>
        <div class="form-field-icon is--select is--success"><!-- success SVG --></div>
        <div class="form-field-icon is--select is--error"><!-- error SVG --></div>
      </div>
    </div>
    <div data-validate="" class="form-field-group">
      <label for="mode" class="form-label">Mode <span class="form-required">*</span></label>
      <div data-radiocheck-group="" class="radiocheck-group">
        <label class="radiocheck-field w-radio">
          <input type="radio" name="Mode" id="Dark-mode" data-name="Mode" class="w-form-formradioinput radio-input w-radio-input" value="Dark mode">
          <span class="radiocheck-label w-form-label" for="Dark-mode">Dark mode</span>
          <div class="radiocheck-custom is--radio"><div class="radio-dot"></div></div>
        </label>
        <label class="radiocheck-field w-radio">
          <input type="radio" name="Mode" id="Light-mode" data-name="Mode" class="w-form-formradioinput radio-input w-radio-input" value="Light mode">
          <span class="radiocheck-label w-form-label" for="Light-mode">Light mode</span>
          <div class="radiocheck-custom is--radio"><div class="radio-dot"></div></div>
        </label>
        <div class="radiocheck-field-icon is--success"><!-- success SVG --></div>
        <div class="radiocheck-field-icon is--error"><!-- error SVG --></div>
      </div>
    </div>
    <div data-validate="" class="form-field-group">
      <label for="message" class="form-label">Message <span class="form-required">*</span></label>
      <div class="form-field">
        <textarea class="form-input is--textarea w-input" maxlength="5000" name="message" data-name="Message" min="3" placeholder="Hello Osmo, " id="message" required=""></textarea>
        <div class="form-field-icon is--success"><!-- success SVG --></div>
        <div class="form-field-icon is--error"><!-- error SVG --></div>
      </div>
    </div>
    <div class="form-field-group">
      <div class="form-divider"></div>
    </div>
    <div data-validate="" class="form-field-group">
      <div data-radiocheck-group="" class="radiocheck-group">
        <label class="w-checkbox radiocheck-field">
          <input type="checkbox" name="Terms-Conditions" id="Terms-Conditions" data-name="Terms Conditions" required="" class="w-checkbox-input checkbox-input">
          <span class="radiocheck-label is--small w-form-label" for="Terms-Conditions">Accept the Terms and Conditions <span class="form-required">*</span></span>
          <div class="radiocheck-custom"><!-- check SVG --></div>
          <div class="radiocheck-field-icon is--success"><!-- success SVG --></div>
          <div class="radiocheck-field-icon is--error"><!-- error SVG --></div>
        </label>
      </div>
    </div>
    <div class="form-field-group">
      <div class="form-field">
        <div data-submit="" tabindex="0" class="form-submit-btn">
          <p class="form-submit-btn-p">Submit</p>
          <input type="submit" data-wait="Please wait..." class="form-submit w-button" value="Submit">
        </div>
      </div>
    </div>
  </form>
  <div class="form-notifcation w-form-done">
    <div class="form-notification-bg"></div>
    <div class="form-notification-p">Success! We'll be in touch soon.</div>
    <div class="form-notification-icon"><!-- success SVG --></div>
  </div>
  <div class="form-notifcation is--error w-form-fail">
    <div class="form-notification-bg"></div>
    <div class="form-notification-p">Something went wrong while submitting.</div>
    <div class="form-notification-icon"><!-- error SVG --></div>
  </div>
</div>
CSS
css
.form-group {
  grid-column-gap: 1.5em;
  grid-row-gap: 1.5em;
  flex-flow: column;
  width: 25em;
  margin-bottom: 0;
  display: flex;
}

.form {
  grid-column-gap: 1.5em;
  grid-row-gap: 1.5em;
  flex-flow: column;
  width: 100%;
  display: flex;
}

.form-field-group {
  grid-column-gap: .75em;
  grid-row-gap: .75em;
  flex-flow: column;
  align-items: flex-start;
  display: flex;
}

.form-field {
  width: 100%;
  position: relative;
}

.form-label {
  color: #131313;
  width: 100%;
  margin-bottom: 0;
  font-size: .875em;
  font-weight: 500;
  line-height: 1;
}

.form-required {
  color: #ff4c24;
}

.form-input {
  outline-offset: 0px;
  color: #131313;
  -webkit-appearance: none;
  appearance: none;
  box-sizing: border-box;
  vertical-align: middle;
  background-color: #efeeec;
  border: 1px solid #efeeec;
  border-radius: .328125em;
  outline: 0 #0000;
  height: auto;
  margin-bottom: 0;
  padding: .9em 3.5em .9em 1em;
  font-size: 1.125em;
  font-weight: 500;
  line-height: 1.2;
  box-shadow: 0 0 #0000;
}

.form-input.is--textarea {
  resize: vertical;
  min-height: 9em;
}

.form-input:focus {
  border-color: #cbc8c5;
}

.form-input::placeholder {
  color: #1313134d;
  background-color: #efeeec;
}

.form-field-icon {
  opacity: 0;
  pointer-events: none;
  color: #cbc8c5;
  -webkit-user-select: none;
  user-select: none;
  border-radius: .375em;
  justify-content: center;
  align-items: center;
  width: 3.5em;
  max-height: 3.5em;
  padding-left: 1em;
  padding-right: 1em;
  display: flex;
  position: absolute;
  top: 1px;
  bottom: 1px;
  right: 1px;
}

.form-field-icon.is--error {
  color: #ff4c24;
}

.form-field-icon.is--select {
  right: 1.75em;
}

.radio-dot {
  color: inherit;
  background-color: currentColor;
  border-radius: 50%;
  width: .375em;
  height: .375em;
}

.radiocheck-field-icon {
  opacity: 0;
  pointer-events: none;
  color: #cbc8c5;
  -webkit-user-select: none;
  user-select: none;
  border-radius: .375em;
  justify-content: center;
  align-items: center;
  width: 3.5em;
  max-height: 3.5em;
  padding-left: 1em;
  padding-right: 1em;
  display: flex;
  position: absolute;
  top: 1px;
  right: 1px;
}

.radiocheck-field-icon.is--error {
  color: #ff4c24;
}

.radiocheck-label {
  color: #131313;
  cursor: pointer;
  flex-grow: 1;
  margin-bottom: 0;
  padding-left: 1.8em;
  font-size: 1.125em;
  font-weight: 500;
  line-height: 1.2;
}

.radiocheck-label.is--small {
  flex-grow: 1;
  padding-left: 2em;
  font-size: 1em;
}

.radiocheck-group {
  grid-column-gap: .75em;
  grid-row-gap: .75em;
  flex-flow: column;
  width: 100%;
  display: flex;
  position: relative;
}

.checkbox-input {
  cursor: pointer;
  width: 0;
  height: 0;
  margin-top: 0;
  margin-left: 0;
  position: absolute;
}

.form-field-chevron {
  pointer-events: none;
  color: #131313;
  -webkit-user-select: none;
  user-select: none;
  border-radius: .375em;
  justify-content: center;
  align-items: center;
  width: 3.5em;
  max-height: 3.5em;
  padding-left: 1em;
  padding-right: 1em;
  display: flex;
  position: absolute;
  top: 1px;
  bottom: 1px;
  right: 1px;
}

.form-inactive-text {
  opacity: .35;
  margin-left: auto;
  padding-left: .5em;
}

.radiocheck-field {
  align-items: flex-start;
  margin-bottom: 0;
  padding-left: 0;
  display: flex;
  position: relative;
}

.radio-input {
  cursor: pointer;
  width: 0;
  height: 0;
  margin-top: 0;
  margin-left: 0;
  position: absolute;
}

.radiocheck-custom {
  pointer-events: none;
  color: #efeeec;
  -webkit-user-select: none;
  user-select: none;
  background-color: #efeeec;
  border: 1px solid #131313;
  border-radius: .125em;
  order: -1;
  justify-content: center;
  align-items: center;
  width: 1.25em;
  height: 1.25em;
  margin-top: .066em;
  margin-right: .75em;
  display: flex;
  position: absolute;
}

.radiocheck-check-svg {
  width: 1.5em;
  position: absolute;
}

.radiocheck-custom.is--radio {
  border-radius: 50%;
}

.form-submit {
  visibility: hidden;
  opacity: 0;
  position: absolute;
  inset: 0;
}

.form-submit-btn {
  outline-offset: 0px;
  color: #efeeec;
  cursor: pointer;
  background-color: #131313;
  border: 1px solid #131313;
  border-radius: .375rem;
  outline: 0 #0000;
  flex-flow: row;
  justify-content: flex-start;
  align-items: center;
  padding: 1.005em 1.125em;
  display: flex;
  position: relative;
  overflow: hidden;
  box-shadow: inset 0 0 #0000;
}

.form-submit-btn:focus {
  outline-offset: 0px;
  border-width: 1px;
  border-color: #cbc8c5;
  outline-color: #131313;
}

.form-submit-btn-p {
  margin-bottom: 0;
  font-size: 1.125em;
  font-weight: 500;
  line-height: 1.2;
}

.form-divider {
  opacity: .15;
  background-color: #131313;
  width: 100%;
  height: 1px;
}

.form-notifcation {
  outline-offset: 0px;
  color: #0ba954;
  text-align: left;
  border: 1px solid #0ba954;
  border-color: inherit;
  background-color: #efeeec;
  border-radius: .375rem;
  outline: 0 #0000;
  width: 100%;
  padding: 1.125em;
  font-size: 1em;
  position: relative;
}

.form-notifcation.is--error {
  color: #ff4c24;
  margin-top: 0;
  padding-right: 3.5em;
}

.form-notification-icon {
  pointer-events: none;
  color: inherit;
  justify-content: center;
  align-items: center;
  width: 3.5em;
  padding-left: 1em;
  padding-right: 1em;
  display: flex;
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
}

.form-notification-bg {
  opacity: .1;
  pointer-events: none;
  color: inherit;
  background-color: currentColor;
  border-radius: calc(.375rem - 2px);
  display: flex;
  position: absolute;
  inset: 0;
}

.form-notification-p {
  color: inherit;
  font-size: 1.125em;
  font-weight: 500;
}

@media screen and (max-width: 767px) {
  .form-group {
    width: 100%;
  }
}

/* Field: Error */
[data-validate].is--error input,
[data-validate].is--error textarea,
[data-validate].is--error select {
  border-color: #FF4C24;
}

[data-validate].is--error .form-field-icon.is--error,
[data-validate].is--error .radiocheck-field-icon.is--error {
  opacity: 1;
}

/* Field: Success */
[data-validate].is--success .form-field-icon.is--success,
[data-validate].is--success .radiocheck-field-icon.is--success {
  opacity: 1;
}

/* Field: Custom Radio or Checkbox */
[data-form-validate] .radiocheck-field input:focus-visible ~ .radiocheck-custom {
  background-color: #D0CFCD;
  color: #E2E1DF;
}

[data-form-validate] .radiocheck-field input:focus-visible:checked ~ .radiocheck-custom,
[data-form-validate] .radiocheck-field input:checked ~ .radiocheck-custom {
  background-color: #131313;
  color: #EFEEEC;
}

[data-form-validate] .radiocheck-field .radiocheck-label.is--small {
  margin-top: 0.125em;
}

[data-validate].is--error .radiocheck-custom {
  border-color: #FF4C24;
}

[data-validate].is--error input:checked ~ .radiocheck-custom {
  border-color: #131313;
}

/* Field: Select */
[data-form-validate] select:has(option[value=""]:checked) {
  color: rgba(19, 19, 19, 0.3);
}
JavaScript
javascript
function initAdvancedFormValidation() {
  const forms = document.querySelectorAll('[data-form-validate]');

  forms.forEach((formContainer) => {
    const startTime = new Date().getTime();

    const form = formContainer.querySelector('form');
    if (!form) return;

    const validateFields = form.querySelectorAll('[data-validate]');
    const dataSubmit = form.querySelector('[data-submit]');
    if (!dataSubmit) return;

    const realSubmitInput = dataSubmit.querySelector('input[type="submit"]');
    if (!realSubmitInput) return;

    function isSpam() {
      const currentTime = new Date().getTime();
      return currentTime - startTime < 5000;
    }

    // Disable select options with invalid values on page load
    validateFields.forEach(function (fieldGroup) {
      const select = fieldGroup.querySelector('select');
      if (select) {
        const options = select.querySelectorAll('option');
        options.forEach(function (option) {
          if (
            option.value === '' ||
            option.value === 'disabled' ||
            option.value === 'null' ||
            option.value === 'false'
          ) {
            option.setAttribute('disabled', 'disabled');
          }
        });
      }
    });

    function validateAndStartLiveValidationForAll() {
      let allValid = true;
      let firstInvalidField = null;

      validateFields.forEach(function (fieldGroup) {
        const input = fieldGroup.querySelector('input, textarea, select');
        const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
        if (!input && !radioCheckGroup) return;

        if (input) input.__validationStarted = true;
        if (radioCheckGroup) {
          radioCheckGroup.__validationStarted = true;
          const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
          inputs.forEach(function (input) {
            input.__validationStarted = true;
          });
        }

        updateFieldStatus(fieldGroup);

        if (!isValid(fieldGroup)) {
          allValid = false;
          if (!firstInvalidField) {
            firstInvalidField = input || radioCheckGroup.querySelector('input');
          }
        }
      });

      if (!allValid && firstInvalidField) {
        firstInvalidField.focus();
      }

      return allValid;
    }

    function isValid(fieldGroup) {
      const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
      if (radioCheckGroup) {
        const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
        const checkedInputs = radioCheckGroup.querySelectorAll('input:checked');
        const min = parseInt(radioCheckGroup.getAttribute('min')) || 1;
        const max = parseInt(radioCheckGroup.getAttribute('max')) || inputs.length;
        const checkedCount = checkedInputs.length;

        if (inputs[0].type === 'radio') {
          return checkedCount >= 1;
        } else {
          if (inputs.length === 1) {
            return inputs[0].checked;
          } else {
            return checkedCount >= min && checkedCount <= max;
          }
        }
      } else {
        const input = fieldGroup.querySelector('input, textarea, select');
        if (!input) return false;

        let valid = true;
        const min = parseInt(input.getAttribute('min')) || 0;
        const max = parseInt(input.getAttribute('max')) || Infinity;
        const value = input.value.trim();
        const length = value.length;

        if (input.tagName.toLowerCase() === 'select') {
          if (value === '' || value === 'disabled' || value === 'null' || value === 'false') {
            valid = false;
          }
        } else if (input.type === 'email') {
          const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
          valid = emailPattern.test(value);
        } else {
          if (input.hasAttribute('min') && length < min) valid = false;
          if (input.hasAttribute('max') && length > max) valid = false;
        }

        return valid;
      }
    }

    function updateFieldStatus(fieldGroup) {
      const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');
      if (radioCheckGroup) {
        const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
        const checkedInputs = radioCheckGroup.querySelectorAll('input:checked');

        if (checkedInputs.length > 0) {
          fieldGroup.classList.add('is--filled');
        } else {
          fieldGroup.classList.remove('is--filled');
        }

        const valid = isValid(fieldGroup);

        if (valid) {
          fieldGroup.classList.add('is--success');
          fieldGroup.classList.remove('is--error');
        } else {
          fieldGroup.classList.remove('is--success');
          const anyStarted = Array.from(inputs).some(input => input.__validationStarted);
          if (anyStarted) {
            fieldGroup.classList.add('is--error');
          } else {
            fieldGroup.classList.remove('is--error');
          }
        }
      } else {
        const input = fieldGroup.querySelector('input, textarea, select');
        if (!input) return;

        if (input.value.trim()) {
          fieldGroup.classList.add('is--filled');
        } else {
          fieldGroup.classList.remove('is--filled');
        }

        const valid = isValid(fieldGroup);

        if (valid) {
          fieldGroup.classList.add('is--success');
          fieldGroup.classList.remove('is--error');
        } else {
          fieldGroup.classList.remove('is--success');
          if (input.__validationStarted) {
            fieldGroup.classList.add('is--error');
          } else {
            fieldGroup.classList.remove('is--error');
          }
        }
      }
    }

    validateFields.forEach(function (fieldGroup) {
      const input = fieldGroup.querySelector('input, textarea, select');
      const radioCheckGroup = fieldGroup.querySelector('[data-radiocheck-group]');

      if (radioCheckGroup) {
        const inputs = radioCheckGroup.querySelectorAll('input[type="radio"], input[type="checkbox"]');
        inputs.forEach(function (input) {
          input.__validationStarted = false;

          input.addEventListener('change', function () {
            requestAnimationFrame(function () {
              if (!input.__validationStarted) {
                const checkedCount = radioCheckGroup.querySelectorAll('input:checked').length;
                const min = parseInt(radioCheckGroup.getAttribute('min')) || 1;
                if (checkedCount >= min) {
                  input.__validationStarted = true;
                }
              }
              if (input.__validationStarted) {
                updateFieldStatus(fieldGroup);
              }
            });
          });

          input.addEventListener('blur', function () {
            input.__validationStarted = true;
            updateFieldStatus(fieldGroup);
          });
        });
      } else if (input) {
        input.__validationStarted = false;

        if (input.tagName.toLowerCase() === 'select') {
          input.addEventListener('change', function () {
            input.__validationStarted = true;
            updateFieldStatus(fieldGroup);
          });
        } else {
          input.addEventListener('input', function () {
            const value = input.value.trim();
            const length = value.length;
            const min = parseInt(input.getAttribute('min')) || 0;
            const max = parseInt(input.getAttribute('max')) || Infinity;

            if (!input.__validationStarted) {
              if (input.type === 'email') {
                if (isValid(fieldGroup)) input.__validationStarted = true;
              } else {
                if (
                  (input.hasAttribute('min') && length >= min) ||
                  (input.hasAttribute('max') && length <= max)
                ) {
                  input.__validationStarted = true;
                }
              }
            }

            if (input.__validationStarted) {
              updateFieldStatus(fieldGroup);
            }
          });

          input.addEventListener('blur', function () {
            input.__validationStarted = true;
            updateFieldStatus(fieldGroup);
          });
        }
      }
    });

    dataSubmit.addEventListener('click', function () {
      if (validateAndStartLiveValidationForAll()) {
        if (isSpam()) {
          alert('Form submitted too quickly. Please try again.');
          return;
        }
        realSubmitInput.click();
      }
    });

    form.addEventListener('keydown', function (event) {
      if (event.key === 'Enter' && event.target.tagName !== 'TEXTAREA') {
        event.preventDefault();
        if (validateAndStartLiveValidationForAll()) {
          if (isSpam()) {
            alert('Form submitted too quickly. Please try again.');
            return;
          }
          realSubmitInput.click();
        }
      }
    });
  });
}

// Initialize
document.addEventListener('DOMContentLoaded', () => {
  initAdvancedFormValidation();
});

Attributes

NameTypeDefaultDescription
data-form-validateattributeAdd to the outer container (parent of the <form> element). Signals the script where to find the form.
data-validateattributeAdd to each field group (label + input/textarea/select). Required on every group that needs validation.
data-radiocheck-groupattributeWrap checkbox or radio button groups in a container with this attribute to enable group validation.
data-submitattributeWrap the submit button. The script intercepts the click, validates all fields, then triggers the real submit only if valid.
minnumberOn <input> / <textarea>: minimum character length. On [data-radiocheck-group]: minimum number of selections required.
maxnumberOn <input> / <textarea>: maximum character length. On [data-radiocheck-group]: maximum number of selections allowed.

Notes

  • Fields validate live as users interact — on input, change, and blur events.
  • Validation state (.is--error / .is--success) only shows after the user first interacts with that specific field.
  • Anti-spam: submission is blocked if the form is submitted in less than 5 seconds after page load.
  • Pressing Enter in any input (except <textarea>) triggers the full validation and custom submit logic.
  • Dynamic classes: .is--error (fails validation), .is--success (passes validation), .is--filled (field has a value).
  • Select options with empty, null, disabled, or false values are automatically disabled on page load.
  • For radio buttons: at least one option must be selected to pass validation.
  • For checkboxes: use [min] and [max] on [data-radiocheck-group] to define required selection range.
  • A simpler version without some of these features is also available: Live Form Validation (Basic).