<h1>Form tip: Focus fields with errors</h1>
<form action="#" method="POST">
  <p>
    <label for="name">What’s Your Name?</label>
    <input id="name"
           name="name"
           required
           aria-required="true"
           data-error-required="Please enter your name"
           >
  </p>
  <p>
    <label for="email">What’s Your Email?</label>
    <input type="email"
           id="email"
           name="email"
           required
           aria-required="true"
           aria-describedby="description-email"
           data-error-required="Please enter your email"
           data-error-invalid="Your email doesn’t look right"
           >
    <small id="description-email">Your email address will only be used to respond to your message and will not be shared with anyone, ever.</small>
  </p>
  <p>
    <label for="message">What’s the message?</label>
    <textarea id="message"
              name="message"
              required
              aria-required="true"
              data-error-required="Please don’t forget to send a message"
              ></textarea>
  </p>
  <p><button type="submit">Send it in</button>
</form>
body {
  margin: 1rem;
}
form {
  max-width: 300px;
}
label {
  display: block;
  font-weight: bold
}
input, select, textarea {
  display: block;
  width: 100%;
  margin: .35em 0;
}
input[type="radio"],
input[type="checkbox"] {
  width: auto;
}
textarea {
  min-height: 100px;
}
input ~ small,
textarea ~ small {
  display: block;
  font-style: italic;
}
input ~ strong,
textarea ~ strong {
  display: block;
  font-size: .9rem;
  font-weight: normal;
  color: red;
  text-align: right;
  margin: 0 0 .25rem;
}
View Compiled
var $validation_error = document.createElement('strong');
$validation_error.className = 'form-validation-error';

function resetValidation( $field, $parent ) {
  $field.removeAttribute('aria-invalid');
  $field.removeAttribute('aria-errormessage');
  $parent.querySelectorAll('.' + $validation_error.className)
         .forEach(function($el){
           $parent.removeChild($el);
         });
}

function isValid( $field, refocus ) {
  refocus = refocus || false;
  
  var validity = $field.validity,
      $parent = $field.parentNode,
      $message = false,
      message = '',
      message_id = $field.id + '-validation-error';
  
  resetValidation( $field, $parent );
  
  if ( validity.valid ) {
    // valid
    return true;
  }
  
  $message = $validation_error.cloneNode(true);
  $message.id = message_id;

  if ( $field.validity.valueMissing ) {
    message = $field.dataset.errorRequired || 'You forgot to fill in this field';
  }
  else {
    message = $field.dataset.errorInvalid || 'The value you submitted doesn’t look right';
  }
  $message.innerHTML = message;
  
  if ( $field.nextElementSibling ) {
    $parent.insertBefore($message, $field.nextElementSibling);
  }
  else {
    $parent.appendChild($message);
  }
  
  $field.setAttribute('aria-errormessage', message_id);
  $field.setAttribute('aria-invalid', 'true');
  
  if (refocus) {
    $field.focus();
  }
  
  // not valid
  return false;
}

function validateMe( e ) {
  var $form = e.target,
      i = 0,
      field_count = $form.elements.length,
      $first_error = false;
  
  for ( i; i< field_count; i++) {
    var $field = $form.elements[i],
        valid = isValid($field);
    if ( !$first_error && !valid ) {
      $first_error = $field;
    }
  }

  if ( $first_error ){
    e.preventDefault();
    $first_error.focus();    
  }
}

var $forms = document.querySelectorAll('form');

$forms.forEach(function($form){
  $form.setAttribute('novalidate','');
  $form.addEventListener('submit', validateMe, false);
  var field_count = $form.elements.length;
  while (field_count--) {
    $form.elements[field_count].addEventListener('change', function(e){
      isValid(e.target, true);
    }, false)
  }
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.