Form Validation

Bootstrap 4 doesn't run its own validation logic — it styles the browser's native HTML5 constraint validation (the required, pattern, type, min/max attributes you already know) using the CSS :invalid/:valid pseudo-classes. You write normal constraint attributes on your inputs, add one class to the form once it's been submitted, and Bootstrap's CSS takes care of coloring fields green or red and showing the matching feedback message. We'll add this to the trail name field on TrailMap's submission form.

Good news for migration: this part of Bootstrap did not change between versions. Bootstrap 5 uses the exact same :invalid/:valid + .was-validated mechanism described on this page — it's one of the few form-related areas where Bootstrap 4 and Bootstrap 5 markup is fully interchangeable.

Triggering Styles: .was-validated

Browsers don't apply :invalid styling until a form has been interacted with, which would otherwise make every empty required field look broken before the user even starts typing. Bootstrap works around this by staying inert until you add .was-validated to the <form> element — only then do the :valid/:invalid selectors kick in and color each field.

<form class="was-validated">
  <input type="text" class="form-control" required>
</form>

Feedback Messages: .valid-feedback / .invalid-feedback

Place a .valid-feedback and/or .invalid-feedback block right after each control. Both start hidden; once .was-validated is active, the browser's native validity state for that specific field decides which one becomes visible.

<input type="text" class="form-control" id="trailName" required>
<div class="valid-feedback">Looks good!</div>
<div class="invalid-feedback">Please enter a trail name.</div>

Adding .was-validated on Submit

In real use you don't hardcode .was-validated — you add it with a small script when the user submits the form, and block the actual submit if the form is still invalid so the browser's native error state also takes over.

// Vanilla JS — attach once the form exists in the DOM
(function () {
  'use strict';
  var form = document.querySelector('#trailForm');
  form.addEventListener('submit', function (event) {
    if (!form.checkValidity()) {
      event.preventDefault();
      event.stopPropagation();
    }
    form.classList.add('was-validated');
  }, false);
})();

Or, the equivalent with jQuery (which Bootstrap 4 already depends on, so it's frequently what you'll see in existing codebases):

$('#trailForm').on('submit', function (e) {
  if (this.checkValidity && !this.checkValidity()) {
    e.preventDefault();
    e.stopPropagation();
  }
  $(this).addClass('was-validated');
});

Worked Example: TrailMap Required Trail Name

Here's the trail name field from TrailMap's submission form with validation wired up — required on the input, both feedback messages in place, and the form tagged with an id the script above hooks into.

<!-- TrailMap: Submit a Trail — required trail name with validation -->
<form id="trailForm" novalidate>
  <div class="form-group">
    <label for="trailName">Trail name</label>
    <input type="text" class="form-control" id="trailName" name="trail_name" required>
    <div class="valid-feedback">Looks good!</div>
    <div class="invalid-feedback">Please enter a trail name.</div>
  </div>
  <button type="submit" class="btn btn-primary">Submit Trail</button>
</form>

The novalidate attribute on the <form> stops the browser from showing its own default validation bubbles, so only Bootstrap's styled feedback appears.

Up next: Input Groups covers attaching icons, text, and buttons directly onto the edge of a form control.