Form Validation

Intermediate
12 min

Form Validation

Bootstrap does not ship a validation library. Instead it styles the browser's built-in constraint validation API: you write standard HTML attributes such as required, minlength and pattern, and Bootstrap draws green and red borders, icons and feedback messages once the form has been checked. This lesson shows the recommended client-side setup, how to render server-side errors with the same styles, and how to display feedback inside input groups and tooltips.

How It Works

Browsers expose :valid and :invalid pseudo-classes on every control, but they apply immediately on page load, which would show every empty required field as an error before the user has typed anything. Bootstrap solves this with two mechanisms:

  1. Styles apply only inside a form that has the class was-validated, which you add after the first submit attempt.
  2. Alternatively, you add is-valid or is-invalid directly to a control, which is what server-rendered pages do.

The feedback text lives in .valid-feedback and .invalid-feedback elements placed right after the control; they are hidden until the matching state is active.

Client-Side Setup

Add novalidate to the form so the browser does not show its own bubbles, and needs-validation as a hook for your script:

html
<form class="needs-validation" novalidate> <div class="mb-3"> <label for="email" class="form-label">Email</label> <input type="email" class="form-control" id="email" required> <div class="valid-feedback">Looks good.</div> <div class="invalid-feedback">A valid email address is required.</div> </div> <div class="mb-3"> <label for="zip" class="form-label">ZIP code</label> <input class="form-control" id="zip" required pattern="[0-9]{6}"> <div class="invalid-feedback">Enter a 6-digit ZIP code.</div> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="agree" required> <label class="form-check-label" for="agree">I accept the terms</label> <div class="invalid-feedback">You must accept before submitting.</div> </div> <button class="btn btn-primary" type="submit">Submit</button> </form>

Then the script from the sample above: on submit, call form.checkValidity(); if it returns false, block the submit; either way add was-validated so the styles show. After that, the browser re-evaluates each field as the user types, so errors clear in real time.

Which Attributes Trigger Validation

| Attribute | Rule | |---|---| | required | Must not be empty (or must be checked) | | minlength / maxlength | Character count limits | | min / max / step | Numeric and date ranges | | pattern | Regular expression the value must match | | type="email", type="url" | Format checks built into the browser |

Custom rules use setCustomValidity(). An empty string clears the error:

javascript
const pwd = document.getElementById("password"); const confirm = document.getElementById("confirm"); confirm.addEventListener("input", () => { confirm.setCustomValidity(confirm.value === pwd.value ? "" : "Passwords do not match"); });

Bootstrap shows the same .invalid-feedback text regardless of which rule failed. If you need per-rule messages, update the feedback element's text in the same handler, or read input.validationMessage.

Server-Side Validation

When the server rejects a submission, re-render the form with is-invalid on the failing controls and is-valid on the passing ones. No was-validated is needed:

html
<div class="mb-3"> <label for="handle" class="form-label">Handle</label> <input class="form-control is-invalid" id="handle" value="ada" aria-describedby="handleError"> <div id="handleError" class="invalid-feedback">That handle is already taken.</div> </div>

Link the input to its message with aria-describedby so screen readers read the error. The same classes work on .form-select, .form-check-input and file inputs.

Feedback in Input Groups and Tooltips

Inside an .input-group, place the feedback element as the last child of the group so it spans the full width:

html
<div class="input-group has-validation"> <span class="input-group-text">@</span> <input class="form-control" required> <div class="invalid-feedback">Username is required.</div> </div>

has-validation fixes the border radius of the last visible control when feedback is shown.

To float the message over the field instead of pushing content down, use .valid-tooltip / .invalid-tooltip and give the parent position-relative:

html
<div class="col-md-4 position-relative"> <input class="form-control" required> <div class="invalid-tooltip">Required.</div> </div>

Common Mistakes

  • Leaving out novalidate. The browser blocks submission and shows its own bubble, so your handler never runs and was-validated is never added.
  • Feedback element in the wrong place. It must be a sibling that immediately follows the control (or the last child of the input group).
  • Adding was-validated on page load. Every required field turns red before interaction.
  • Forgetting to clear setCustomValidity. Once set, the field stays invalid until you call it with an empty string.
Quick Quiz
Question 1 of 3

What does adding the `was-validated` class to a form do?

Key Takeaways

  • Bootstrap styles the browser's constraint validation; rules come from HTML attributes such as required, pattern and minlength.
  • Use novalidate + needs-validation, then add was-validated after calling checkValidity() on submit.
  • .valid-feedback / .invalid-feedback must directly follow the control; use .has-validation inside input groups.
  • Server-side errors use is-valid / is-invalid classes with aria-describedby for accessibility.
  • setCustomValidity("") clears custom errors; any non-empty string marks the field invalid.

Next lesson: Cards - flexible content containers with headers, images, lists and layout options.

Form Validation - Bootstrap | CodeYourCraft | CodeYourCraft