Skip to content

Documentation

On this page

Validation

Bootstrap’s form validation look, driven by :user-valid and :user-invalid after the user types, blurs, or tries to submit. No .was-validated parent, no submit listener, no classList.

Sizes are minified and gzip. A plugin figure includes the Bootstrap runtime that plugin imports. Shipping every plugin is bootstrap.bundle.min.js at 23.3 KB gzip, not the sum of these rows.
What runs it JavaScript
Bootstrap 5.3 No validation plugin. The Bootstrap docs add .was-validated from a submit listener. Not in bootstrap.min.js
This repo :user-valid and :user-invalid 0 KB
Markup this component accepts. The full inventory is /components.json.
Avoid
  • No .was-validated wrapper and no novalidate plus a hand-rolled check.
  • .is-valid / .is-invalid stay, but only for state the server decided.
  • Do not expect a non-required .form-check-input to turn green after a click. :user-valid success on checks is for required only.

How it works

  • :user-invalid / :user-valid replace .was-validated :invalid / :valid. Empty required fields stay unstyled until the user interacts or submits. A non-required checkbox is always valid, so success green on .form-check-input only applies when the control is required.
  • Keep .is-invalid / .is-valid for server-side markup. They do not wait for interaction.
  • Do not add novalidate. Without script, that attribute lets an invalid form submit. Leave it off and the browser still blocks submit and may show its own bubble on the first invalid field.
  • Custom .invalid-feedback / .valid-feedback (and the tooltip variants) must come after the control. CSS cannot style a label that sits before the input.
  • .has-validation on an input group is only for border radius when feedback is a sibling. It does not turn validation on.
  • setCustomValidity still needs JavaScript. Native constraints (required, type, pattern, min / max, minlength / maxlength) do not.

Custom styles

Change a field and leave it, or press Submit. Borders, icons, and .valid-feedback / .invalid-feedback appear after that interaction — the same look as Bootstrap’s was-validated demo, without the class or the script.

Looks good!
Looks good!
@
Please choose a username.
Please provide a valid city.
Please select a valid state.
Please provide a valid zip.
You must agree before submitting.
HTML
<form class="row g-3">
  <div class="col-md-4">
    <label for="validationCustom01" class="form-label">First name</label>
    <input type="text" class="form-control" id="validationCustom01" value="Mark" autocomplete="given-name" aria-describedby="validationCustom01Feedback" required>
    <div id="validationCustom01Feedback" class="valid-feedback">
      Looks good!
    </div>
  </div>
  <div class="col-md-4">
    <label for="validationCustom02" class="form-label">Last name</label>
    <input type="text" class="form-control" id="validationCustom02" value="Otto" autocomplete="family-name" aria-describedby="validationCustom02Feedback" required>
    <div id="validationCustom02Feedback" class="valid-feedback">
      Looks good!
    </div>
  </div>
  <div class="col-md-4">
    <label for="validationCustomUsername" class="form-label">Username</label>
    <div class="input-group has-validation">
      <span class="input-group-text" id="inputGroupPrepend">@</span>
      <input type="text" class="form-control" id="validationCustomUsername" aria-describedby="inputGroupPrepend validationCustomUsernameFeedback" required>
      <div id="validationCustomUsernameFeedback" class="invalid-feedback">
        Please choose a username.
      </div>
    </div>
  </div>
  <div class="col-md-6">
    <label for="validationCustom03" class="form-label">City</label>
    <input type="text" class="form-control" id="validationCustom03" autocomplete="address-level2" aria-describedby="validationCustom03Feedback" required>
    <div id="validationCustom03Feedback" class="invalid-feedback">
      Please provide a valid city.
    </div>
  </div>
  <div class="col-md-3">
    <label for="validationCustom04" class="form-label">State</label>
    <select class="form-select" id="validationCustom04" aria-describedby="validationCustom04Feedback" required>
      <option selected disabled value="">Choose...</option>
      <option>California</option>
      <option>Texas</option>
    </select>
    <div id="validationCustom04Feedback" class="invalid-feedback">
      Please select a valid state.
    </div>
  </div>
  <div class="col-md-3">
    <label for="validationCustom05" class="form-label">Zip</label>
    <input type="text" class="form-control" id="validationCustom05" autocomplete="postal-code" aria-describedby="validationCustom05Feedback" required>
    <div id="validationCustom05Feedback" class="invalid-feedback">
      Please provide a valid zip.
    </div>
  </div>
  <div class="col-12">
    <div class="form-check">
      <input class="form-check-input" type="checkbox" value="" id="invalidCheck" aria-describedby="invalidCheckFeedback" required>
      <label class="form-check-label" for="invalidCheck">
        Agree to terms and conditions
      </label>
      <div id="invalidCheckFeedback" class="invalid-feedback">
        You must agree before submitting.
      </div>
    </div>
  </div>
  <div class="col-12">
    <button class="btn btn-primary" type="submit">Submit form</button>
  </div>
</form>

Server-side

When the server already knows the result, mark controls with .is-valid / .is-invalid. Associate each error with aria-describedby.

Looks good!
Looks good!
@
Please choose a username.
Please provide a valid city.
Please select a valid state.
Please provide a valid zip.
You must agree before submitting.
HTML
<form class="row g-3">
  <div class="col-md-4">
    <label for="validationServer01" class="form-label">First name</label>
    <input type="text" class="form-control is-valid" id="validationServer01" value="Mark" required>
    <div class="valid-feedback">
      Looks good!
    </div>
  </div>
  <div class="col-md-4">
    <label for="validationServer02" class="form-label">Last name</label>
    <input type="text" class="form-control is-valid" id="validationServer02" value="Otto" required>
    <div class="valid-feedback">
      Looks good!
    </div>
  </div>
  <div class="col-md-4">
    <label for="validationServerUsername" class="form-label">Username</label>
    <div class="input-group has-validation">
      <span class="input-group-text" id="inputGroupPrepend3">@</span>
      <input type="text" class="form-control is-invalid" id="validationServerUsername" aria-describedby="inputGroupPrepend3 validationServerUsernameFeedback" required>
      <div id="validationServerUsernameFeedback" class="invalid-feedback">
        Please choose a username.
      </div>
    </div>
  </div>
  <div class="col-md-6">
    <label for="validationServer03" class="form-label">City</label>
    <input type="text" class="form-control is-invalid" id="validationServer03" aria-describedby="validationServer03Feedback" required>
    <div id="validationServer03Feedback" class="invalid-feedback">
      Please provide a valid city.
    </div>
  </div>
  <div class="col-md-3">
    <label for="validationServer04" class="form-label">State</label>
    <select class="form-select is-invalid" id="validationServer04" aria-describedby="validationServer04Feedback" required>
      <option selected disabled value="">Choose...</option>
      <option>...</option>
    </select>
    <div id="validationServer04Feedback" class="invalid-feedback">
      Please select a valid state.
    </div>
  </div>
  <div class="col-md-3">
    <label for="validationServer05" class="form-label">Zip</label>
    <input type="text" class="form-control is-invalid" id="validationServer05" aria-describedby="validationServer05Feedback" required>
    <div id="validationServer05Feedback" class="invalid-feedback">
      Please provide a valid zip.
    </div>
  </div>
  <div class="col-12">
    <div class="form-check">
      <input class="form-check-input is-invalid" type="checkbox" value="" id="invalidCheck3" aria-describedby="invalidCheck3Feedback" required>
      <label class="form-check-label" for="invalidCheck3">
        Agree to terms and conditions
      </label>
      <div id="invalidCheck3Feedback" class="invalid-feedback">
        You must agree before submitting.
      </div>
    </div>
  </div>
  <div class="col-12">
    <button class="btn btn-primary" type="submit">Submit form</button>
  </div>
</form>

Tooltips

Swap .{valid|invalid}-feedback for .{valid|invalid}-tooltip. The parent needs position: relative (column classes already have it).

Looks good!
Looks good!
@
Please choose a unique and valid username.
Please provide a valid city.
Please select a valid state.
Please provide a valid zip.
HTML
<form class="row g-3">
  <div class="col-md-4 position-relative">
    <label for="validationTooltip01" class="form-label">First name</label>
    <input type="text" class="form-control" id="validationTooltip01" value="Mark" autocomplete="given-name" aria-describedby="validationTooltip01Feedback" required>
    <div id="validationTooltip01Feedback" class="valid-tooltip">
      Looks good!
    </div>
  </div>
  <div class="col-md-4 position-relative">
    <label for="validationTooltip02" class="form-label">Last name</label>
    <input type="text" class="form-control" id="validationTooltip02" value="Otto" autocomplete="family-name" aria-describedby="validationTooltip02Feedback" required>
    <div id="validationTooltip02Feedback" class="valid-tooltip">
      Looks good!
    </div>
  </div>
  <div class="col-md-4 position-relative">
    <label for="validationTooltipUsername" class="form-label">Username</label>
    <div class="input-group has-validation">
      <span class="input-group-text" id="validationTooltipUsernamePrepend">@</span>
      <input type="text" class="form-control" id="validationTooltipUsername" aria-describedby="validationTooltipUsernamePrepend validationTooltipUsernameFeedback" required>
      <div id="validationTooltipUsernameFeedback" class="invalid-tooltip">
        Please choose a unique and valid username.
      </div>
    </div>
  </div>
  <div class="col-md-6 position-relative">
    <label for="validationTooltip03" class="form-label">City</label>
    <input type="text" class="form-control" id="validationTooltip03" autocomplete="address-level2" aria-describedby="validationTooltip03Feedback" required>
    <div id="validationTooltip03Feedback" class="invalid-tooltip">
      Please provide a valid city.
    </div>
  </div>
  <div class="col-md-3 position-relative">
    <label for="validationTooltip04" class="form-label">State</label>
    <select class="form-select" id="validationTooltip04" aria-describedby="validationTooltip04Feedback" required>
      <option selected disabled value="">Choose...</option>
      <option>California</option>
      <option>Texas</option>
    </select>
    <div id="validationTooltip04Feedback" class="invalid-tooltip">
      Please select a valid state.
    </div>
  </div>
  <div class="col-md-3 position-relative">
    <label for="validationTooltip05" class="form-label">Zip</label>
    <input type="text" class="form-control" id="validationTooltip05" autocomplete="postal-code" aria-describedby="validationTooltip05Feedback" required>
    <div id="validationTooltip05Feedback" class="invalid-tooltip">
      Please provide a valid zip.
    </div>
  </div>
  <div class="col-12">
    <button class="btn btn-primary" type="submit">Submit form</button>
  </div>
</form>

Accessibility

Native constraints (required, type, pattern) are exposed by the control. Associate .invalid-feedback with the field through aria-describedby. Put that feedback after the control so :user-invalid can show it.

Do not add novalidate unless you replace the browser’s submit check with your own. Empty required fields stay unstyled until the user interacts or submits.

Browser support

Three columns, not one baseline. The same row is on the overview.
No script Polyfill No fallback
:user-valid / :user-invalid: Chrome 119+, Firefox 88+, Safari 16.5+. None None