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.
| 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 |
| Avoid |
|
|---|
How it works
-
:user-invalid/:user-validreplace.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-inputonly applies when the control isrequired. -
Keep
.is-invalid/.is-validfor 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-validationon an input group is only for border radius when feedback is a sibling. It does not turn validation on. -
setCustomValiditystill 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.
<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.
<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).
<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
| No script | Polyfill | No fallback |
|---|---|---|
:user-valid / :user-invalid: Chrome 119+, Firefox 88+, Safari 16.5+. | None | None |