<form id="first-name-form" action="#" method="GET" novalidate>
  <legend>
    Enter numbers, or more than 10 characters, to see the name-validation fail. Try submitting the form without entering anything.
  </legend>

  <label>
		Your first name:
		<input 
           type="text"
           name="firstname"
           data-validate="required noNumbers maxLength"
           required
           placeholder="e.g. 'Adam'"
           value="">
    <small 
           role="alert" 
           aria-hidden="true"
           data-validation-message="noNumbers">
      Please don't enter any numbers.
    </small>
    <small 
           role="alert" 
           aria-hidden="true"
           data-validation-message="maxLength">
      Please enter 10 characters or fewer.
    </small>
    <small 
           role="alert" 
           aria-hidden="true"
           data-validation-message="required">
      Please enter a name.
    </small>
  </label>
  <label>
    <input type="checkbox" name="acceptTerms" data-validate="mustBeChecked" required> I accept the terms.
    <small role="alert" aria-hidden="true" data-validation-message="mustBeChecked">
      Please accept the terms.
    </small>
  </label>
  <button type="submit">Submit Form</button>
</form>
html {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
}

body {
  width: 100%;
  height: 100%;
  padding: 0;
  margin: 0;
  font-family: helvetica, arial;
  background-image: linear-gradient(to bottom, #eee, #ddd);
  background-size: cover;
  background-repeat: no-repeat;
  display: flex;
  align-items: center;
  justify-content: center;
}

legend {
  font-style: italic;
  padding-bottom: 1rem;
  font-size: 0.9rem;
}

form {
  display: flex;
  flex-direction: column;
  width: 300px;
  padding: 1rem;
  border-radius: 5px;
  background: white;
  box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
  &.invalid {
    border: 2px solid brown;
    box-shadow: 0 0 20px rgba(165, 42, 42, 0.3);
  }
}

[data-validation-message] {
  display: none;
  color: brown;
  font-weight: bold;
}
[data-validation-message].message-visible {
  display: block;
}

label {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin-bottom: 0.75rem;
  small {
    flex: 1;
    min-width: 100%;
    text-align: right;
  }
}

input {
  font-size: 1rem;
  padding: 5px;
  margin-left: 5px;
  border: 1px solid black;
}

input.invalid {
  border-color: brown;
}

button {
  font-size: 0.8rem;
  padding: 10px;
  border-radius: 5px;
  background: linear-gradient(to bottom, #175172, #0d405e);
  color: white;
  margin-top: 1rem;
}
// (1) Defining our validator-functions
//     for later use. They return
//     "true", if the validation passes.
const validators = {
  // Checks the field for emptiness.
  required: element => element.value.length > 0,

  // Checks if there are no numbers
  // in the field.
  noNumbers: element => !element.value.match(/[0-9]/g),

  // Checks if the value is shorter
  // than 10 characters.
  maxLength: element => element.value.length <= 10,

  // Checks if the checkbox is checked.
  mustBeChecked: element => element.checked
};

// (2) Check the contents of an input,
//     get all validators, and mark
//     the field in case of invalidity.
function validateElement(element) {
  resetValidation(element);

  // Store all validators from the
  // data-validate-attribute into
  // an array.
  const rules = element.dataset.validate.split(" ");

  // For every validator on the
  // field...
  rules.forEach(rule => {
    // ...find the corresponding
    // validator-function from our
    // object from #1 and call it
    // with the element as parameter.
    if (validators[rule](element)) {
      // If the function returns true, all is fine.
      return;
    } else {
      // If it returns false, the
      // validation failed.
      // In that case, the
      // markElementInvalid-function
      // takes care of showing the
      // error-message.
      markElementInvalid(element, rule);
    }
  });
}

// (3) Adds classes to an element, 
//     so it appears invalid. Also 
//     it picks the correct feedback-
//     message and sets it visible.
function markElementInvalid(element, validatorName) {
  element.classList.add("invalid");
  element.setAttribute("aria-invalid", true);
  const feedbackMessage = element.parentNode.querySelector(
    `[data-validation-message=${validatorName}]`
  );
  feedbackMessage.classList.add("message-visible");
  feedbackMessage.setAttribute("aria-hidden", false);
}

// (4) Removes all traces of 
//     validation from an element,
//     like the error-messages and 
//     the styling.
//     (Pretty much the opposite of #3)
function resetValidation(element) {
  element.classList.remove("invalid");
  element.setAttribute("aria-invalid", false);
  element.parentNode
    .querySelectorAll("[data-validation-message]")
    .forEach(e => {
      e.classList.remove("message-visible");
      e.setAttribute("aria-hidden", true);
    });
}

// (5) Store the form and its 
//     inputs in variables.
const form = document.getElementById("first-name-form");
const formElements = Array.from(form.elements);

// (6) Every input gets an 
//     event-listener attached.
formElements.forEach(formElement => {
  // Do nothing if the element has 
  // no data-validate-attribute.
  if (!formElement.dataset) return;
  if (!formElement.dataset.validate) return;

  // Attach the blur-event-listener 
  // to the element.
  formElement.addEventListener("blur", () => {
    // This means, validateElement will 
    // be called everytime the element 
    // loses focus.
    validateElement(formElement);
  });
});

// (7) We're doing what we did at #3, 
//     only on form-submit, and for 
//     every field.
form.addEventListener("submit", event => {
  // Let's assume, everything is fine.
  let formIsValid = true;
  form.classList.remove("invalid");

  // We'll check every field in the form. 
  // (same as #3)
  formElements.forEach(formElement => {
    if (!formElement.dataset) return;
    if (!formElement.dataset.validate) return;
    validateElement(formElement);
  });

  // If there are any "invalid"-classes 
  // after we checked all fields, the form 
  // is invalid...
  formIsValid = form.querySelectorAll(".invalid").length === 0;

  // ...and will not be submitted.
  if (formIsValid === false) {
    form.classList.add("invalid");
    event.preventDefault();
  }
});
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.