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>
: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.