Form Validation

"Bootstrap validation" is a common source of confusion: Bootstrap does not check whether a field is filled in correctly. The browser's native HTML5 validation does that, using plain attributes like required, pattern, and type="email". Bootstrap's job is purely visual — it watches the same :valid/:invalid states the browser already computes and styles them with borders, icons, and feedback text. This lesson shows exactly where that line sits, using TrailMap's required trail name field as the example.

How Bootstrap Validation Actually Works

The validation logic itself is 100% native browser behavior driven by HTML5 attributes on the <input> — required, minlength, pattern, or a specific type like email or url. The browser evaluates those and exposes the result as the CSS pseudo-classes :valid and :invalid. Bootstrap ships CSS that styles those two pseudo-classes — but only once a .was-validated class is present on an ancestor <form>. Before that class is added, fields look like ordinary .form-control inputs even if they're technically invalid.

Marking Fields: required, pattern, and type

Reach for plain HTML5 attributes exactly as you would without Bootstrap. Bootstrap adds nothing new here — it only styles what the browser already decides.

<!-- Required text field -->
<input type="text" class="form-control" id="trailName" required>

<!-- Required, must be a valid email -->
<input type="email" class="form-control" id="submitterEmail" required>

<!-- Distance must match a decimal number pattern -->
<input type="text" class="form-control" id="trailDistance" pattern="[0-9]+(\.[0-9]+)?" required>

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

Drop a .valid-feedback and/or .invalid-feedback element right after the control. Both are hidden by default; once .was-validated is active, Bootstrap shows whichever one matches the field's current :valid/:invalid state.

<div class="mb-3">
  <label for="trailName" class="form-label">Trail Name</label>
  <input type="text" class="form-control" id="trailName" required>
  <div class="valid-feedback">Looks good!</div>
  <div class="invalid-feedback">Please enter the trail's name.</div>
</div>
Note: If you'd rather not rely on the :valid/:invalid pseudo-classes (for example, to flag a field invalid from a server-side check), add .is-valid or .is-invalid directly to the input — those work with or without .was-validated on the form.

Triggering the Styles: Adding .was-validated with JS

Because .was-validated only appears once you add it, you need a few lines of vanilla JavaScript that add it on submit and stop the browser navigating away if the form isn't valid yet — this is the only JavaScript involved in the whole process.

<form id="trailForm" novalidate>
  <!-- fields go here -->
</form>

<script>
const form = document.getElementById('trailForm');
form.addEventListener('submit', function (event) {
  if (!form.checkValidity()) {
    event.preventDefault();
    event.stopPropagation();
  }
  form.classList.add('was-validated');
}, false);
</script>

The novalidate attribute on the form stops the browser's own default error bubbles from appearing so Bootstrap's styled feedback shows instead — the browser still runs all the validation checks, it just no longer displays them itself.

Worked Example: TrailMap's Required Trail Name Field

The trail name field with required set, both feedback messages in place, and the submit script wired up — this is what a user sees after clicking "Submit Trail" with the field still empty, then again once they've typed a name:

<form id="trailForm" novalidate>
  <div class="mb-3">
    <label for="trailName" class="form-label">Trail Name</label>
    <input type="text" class="form-control" id="trailName" required>
    <div class="valid-feedback">Looks good!</div>
    <div class="invalid-feedback">Please enter the trail's name.</div>
  </div>
  <button type="submit" class="btn btn-primary">Submit Trail</button>
</form>

Before submit: a plain-looking .form-control. After a failed submit with the field empty: a red border, an exclamation icon, and "Please enter the trail's name." showing below it. Type a name and submit again: a green border, a check icon, and "Looks good!" instead — all from the same markup, driven entirely by the required attribute and the JS snippet above.

Next up: Validation styling works alongside almost any control, including the prepend/append pattern covered in Input Groups next.