Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Bootstrap styles form validation states, but the browser’s built-in constraint validation supplies most of the rules that decide whether a field is valid. The fastest way to get working feedback is to start from the example that matches your control and behavior, then wire it to your own submit handler or server response. Every snippet below uses Bootstrap v5.3 class names and markup, and the official Bootstrap documentation currently identifies v5.3.8 as the documented release.

This is a curated set of 35 examples written for this guide. Bootstrap does not publish a 35-example collection, so the count reflects this article’s selection. Each example is a self-contained starting point grouped by behavior and control type. Run each one in your own page before relying on it in production.

How Bootstrap validation fits together

Bootstrap does not decide what is valid. It styles the result. A field becomes valid or invalid through one of three sources, and the markup you add determines which feedback a reader sees.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Constraint attributes on the control. Attributes such as required, minlength, pattern, min, max, and type='email' let the browser mark a field as :invalid. Bootstrap styles those states through its :invalid and :valid selectors.
  2. A trigger that reveals the styling. Bootstrap scopes its custom display to a .was-validated parent, which you usually add after a submission attempt. Without it, an empty required field would look invalid as soon as the page loads.
  3. Optional JavaScript or server output. JavaScript can call checkValidity() or setCustomValidity(). A server can also return .is-invalid or .is-valid classes directly. Those fallback classes do not need a .was-validated parent.

The HTML Standard remains the normative reference for how browsers handle form controls, so use it when you need to confirm browser-level behavior.

Choose an approach before you choose an example

Approach Where validity comes from How feedback appears When the user sees it Accessibility note from Bootstrap’s documentation
Native browser validation Constraint attributes on the control Browser-drawn message and highlight On submit, unless novalidate is set Browser-provided; the documentation notes the feedback varies by browser and operating system
Bootstrap custom validation Constraint attributes plus a JavaScript checkValidity() call Inline .valid-feedback and .invalid-feedback text After .was-validated is added to the form Client-side custom styles and tooltips are not exposed to assistive technologies
Server-rendered states Server response Inline feedback next to .is-invalid or .is-valid fields As soon as the server returns the page or updated markup Bootstrap recommends this option; connect the message with aria-describedby
Tooltip variant Any of the above .valid-tooltip or .invalid-tooltip positioned over the control Same trigger as the approach it is paired with Same client-side caveat as custom validation

Accessibility limits to read first

Bootstrap’s documentation states: “We are aware that currently the client-side custom validation styles and tooltips are not accessible, since they are not exposed to assistive technologies.” The same page recommends the server-side option or default browser validation while the issue remains open. Treat the custom examples below as a visual layer. Do not treat them as an accessible solution on their own. Where you use them, also make sure the error text itself is announced to users, for example by pairing it with a server-side message linked through aria-describedby.

Why the feedback does not appear

  • The Bootstrap CSS or JavaScript is not loaded. Check the network tab for the Bootstrap 5.3 stylesheet.
  • The .was-validated class is missing. Custom feedback stays hidden until the form (or a parent) carries this class.
  • The feedback element is not a sibling of the field. Bootstrap uses sibling selectors such as .form-control:invalid ~ .invalid-feedback, so the feedback div must come after the input at the same level.
  • The tooltip has no positioned parent. Tooltips need a parent with position: relative. The utility class position-relative sets that.
  • The handler never runs. Confirm that the script loads after the form exists or is wrapped in a DOMContentLoaded listener, and that the submit listener is attached to the right form.

Basic validation patterns

Example 1: Required text field with native browser feedback (Bootstrap v5.3)

<form>
  <div class='mb-3'>
    <label for='name1' class='form-label'>Name</label>
    <input type='text' class='form-control' id='name1' required>
  </div>
  <button type='submit' class='btn btn-primary'>Submit</button>
</form>

Example 2: Bootstrap custom feedback activated on submit (Bootstrap v5.3)

<form id='f2' novalidate>
  <label for='n2' class='form-label'>Name</label>
  <input type='text' class='form-control' id='n2' required>
  <div class='invalid-feedback'>Please enter your name.</div>
  <button class='btn btn-primary mt-3' type='submit'>Submit</button>
</form>
<script>
const f2 = document.getElementById('f2');
f2.addEventListener('submit', function (event) {
  if (!f2.checkValidity()) {
    event.preventDefault();
    event.stopPropagation();
  }
  f2.classList.add('was-validated');
}, false);
</script>

The novalidate attribute suppresses the browser’s own tooltips while leaving the JavaScript validation API available. This is a starter pattern. It does not replace server-side validation.

Example 3: Valid and invalid inline feedback (Bootstrap v5.3)

<form class='was-validated' novalidate>
  <div class='mb-3'>
    <input type='text' class='form-control' id='e3' required>
    <div class='valid-feedback'>Looks good!</div>
    <div class='invalid-feedback'>Please enter a city.</div>
  </div>
</form>

Example 4: Server-rendered invalid state with aria-describedby (Bootstrap v5.3)

<div class='mb-3'>
  <label for='e4' class='form-label'>Email</label>
  <input type='email' class='form-control is-invalid' id='e4' aria-describedby='e4-error'>
  <div id='e4-error' class='invalid-feedback'>That email address is already registered.</div>
</div>

The .is-invalid class works without a .was-validated parent, which makes it suitable for states your server returns.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example 5: Server-rendered valid state (Bootstrap v5.3)

<div class='mb-3'>
  <label for='u5' class='form-label'>Username</label>
  <input type='text' class='form-control is-valid' id='u5' value='jdoe' aria-describedby='u5-ok'>
  <div id='u5-ok' class='valid-feedback'>Username is available.</div>
</div>

Input types and constraint attributes

The forms documentation recommends choosing the right input type, such as email or number, because the browser then supplies relevant controls and built-in validation.

Example 6: Email field (Bootstrap v5.3)

<input type='email' class='form-control' id='e6' required>
<div class='invalid-feedback'>Enter an email address such as name@example.com.</div>

Example 7: Password with minimum length and a digit (Bootstrap v5.3)

<input type='password' class='form-control' id='p7' minlength='8' pattern='.*[0-9].*' required>
<div class='invalid-feedback'>Use at least 8 characters, including one number.</div>

Example 8: Number with minimum and maximum (Bootstrap v5.3)

<input type='number' class='form-control' id='n8' min='1' max='10' step='1' required>
<div class='invalid-feedback'>Enter a whole number from 1 to 10.</div>

Example 9: Telephone with a pattern (Bootstrap v5.3)

<input type='tel' class='form-control' id='t9' pattern='[0-9]{3}-[0-9]{4}' placeholder='555-0100' required>
<div class='invalid-feedback'>Use the format 555-0100.</div>

Example 10: URL field (Bootstrap v5.3)

<input type='url' class='form-control' id='w10' required>
<div class='invalid-feedback'>Enter a full web address, including https://.</div>

Textarea and select controls

Example 11: Textarea with length limits (Bootstrap v5.3)

<textarea class='form-control' id='ta11' rows='4' minlength='20' maxlength='500' required></textarea>
<div class='invalid-feedback'>Write between 20 and 500 characters.</div>

Example 12: Select with an empty placeholder option (Bootstrap v5.3)

<select class='form-select' id='s12' required>
  <option value=''>Choose...</option>
  <option value='uk'>United Kingdom</option>
  <option value='us'>United States</option>
</select>
<div class='invalid-feedback'>Select a country.</div>

The empty value='' option is what makes the placeholder invalid. Without it, the first real option is selected and passes validation.

Example 13: Multiple-select list (Bootstrap v5.3)

<select class='form-select' id='m13' multiple size='3' required>
  <option value='html'>HTML</option>
  <option value='css'>CSS</option>
  <option value='js'>JavaScript</option>
</select>
<div class='invalid-feedback'>Choose at least one skill.</div>

Checkboxes and radio groups

Example 14: Single required checkbox (Bootstrap v5.3)

<div class='form-check'>
  <input class='form-check-input' type='checkbox' id='c14' required>
  <label class='form-check-label' for='c14'>I accept the terms</label>
  <div class='invalid-feedback'>You must accept the terms.</div>
</div>

Example 15: Checkbox group that needs at least one selection (Bootstrap v5.3)

<div class='form-check'>
  <input class='form-check-input interest' type='checkbox' id='i15a'>
  <label class='form-check-label' for='i15a'>Email updates</label>
</div>
<div class='form-check'>
  <input class='form-check-input interest' type='checkbox' id='i15b'>
  <label class='form-check-label' for='i15b'>SMS alerts</label>
  <div class='invalid-feedback'>Pick at least one option.</div>
</div>
<script>
function syncInterests() {
  const any = [...document.querySelectorAll('.interest')].some(function (box) { return box.checked; });
  document.querySelectorAll('.interest').forEach(function (box) {
    box.setCustomValidity(any ? '' : 'Pick at least one option.');
  });
}
document.querySelectorAll('.interest').forEach(function (box) {
  box.addEventListener('change', syncInterests);
});
syncInterests();
</script>

Place the feedback after the last checkbox in the group, because that is the sibling the validation selector reaches.

Example 16: Required radio group (Bootstrap v5.3)

<div class='form-check'>
  <input class='form-check-input' type='radio' name='plan16' id='p16a' required>
  <label class='form-check-label' for='p16a'>Monthly</label>
</div>
<div class='form-check'>
  <input class='form-check-input' type='radio' name='plan16' id='p16b'>
  <label class='form-check-label' for='p16b'>Annual</label>
  <div class='invalid-feedback'>Choose a billing plan.</div>
</div>

Example 17: Server-marked radio group (Bootstrap v5.3)

<div class='form-check'>
  <input class='form-check-input is-invalid' type='radio' name='plan17' id='p17a'>
  <label class='form-check-label' for='p17a'>Monthly</label>
</div>
<div class='form-check'>
  <input class='form-check-input is-invalid' type='radio' name='plan17' id='p17b' aria-describedby='p17-error'>
  <label class='form-check-label' for='p17b'>Annual</label>
  <div id='p17-error' class='invalid-feedback'>The selected plan is no longer available.</div>
</div>

File inputs

Example 18: Required file with an accepted type list (Bootstrap v5.3)

<input type='file' class='form-control' id='f18' accept='.pdf,.docx' required>
<div class='invalid-feedback'>Upload a PDF or Word document.</div>

The accept attribute guides the file picker, but the server must still check the file type it receives.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example 19: Multiple files with a size limit (Bootstrap v5.3)

<input type='file' class='form-control' id='f19' multiple required>
<div class='invalid-feedback'>Each file must be 5 MB or smaller.</div>
<script>
const fileInput = document.getElementById('f19');
fileInput.addEventListener('change', function () {
  const limit = 5 * 1024 * 1024;
  const tooBig = [...fileInput.files].some(function (file) { return file.size > limit; });
  fileInput.setCustomValidity(tooBig ? 'Each file must be 5 MB or smaller.' : '');
});
</script>

Input groups

Example 20: Input group with has-validation (Bootstrap v5.3)

<div class='input-group has-validation'>
  <span class='input-group-text' id='g20'>@</span>
  <input type='text' class='form-control is-invalid' id='h20' aria-describedby='g20 h20-error' required>
  <div id='h20-error' class='invalid-feedback'>Choose a handle without spaces.</div>
</div>

The .has-validation class on the group is the documented fix for border-radius problems when the feedback is attached to the last element in the group.

Example 21: Currency amount with a prefix (Bootstrap v5.3)

<div class='input-group has-validation'>
  <span class='input-group-text'>$</span>
  <input type='number' class='form-control' id='a21' min='1' step='0.01' required>
  <div class='invalid-feedback'>Enter an amount of at least 1.00.</div>
</div>

Example 22: Input group combining a select and a text field (Bootstrap v5.3)

<div class='input-group has-validation'>
  <select class='form-select' id='sel22' required>
    <option value=''>Code</option>
    <option value='+44'>+44</option>
    <option value='+1'>+1</option>
  </select>
  <input type='tel' class='form-control' id='ph22' pattern='[0-9]{7,10}' required>
  <div class='invalid-feedback'>Choose a dialing code and enter 7 to 10 digits.</div>
</div>

Tooltip feedback

Tooltips use .valid-tooltip or .invalid-tooltip. Each example below places the parent wrapper with position-relative. The client-side caveat from the accessibility section applies to every tooltip.

Example 23: Invalid tooltip on a text field (Bootstrap v5.3)

<div class='position-relative'>
  <input type='text' class='form-control' id='t23' required>
  <div class='invalid-tooltip'>Enter your postcode.</div>
</div>

Example 24: Valid tooltip on a text field (Bootstrap v5.3)

<div class='position-relative'>
  <input type='text' class='form-control is-valid' id='t24' value='SW1A 1AA'>
  <div class='valid-tooltip'>Postcode accepted.</div>
</div>

Example 25: Invalid tooltip on a select (Bootstrap v5.3)

<div class='position-relative'>
  <select class='form-select' id='t25' required>
    <option value=''>Choose...</option>
    <option value='yes'>Yes</option>
  </select>
  <div class='invalid-tooltip'>Select an option.</div>
</div>

Example 26: Tooltip on a file input (Bootstrap v5.3)

<div class='position-relative'>
  <input type='file' class='form-control' id='t26' required>
  <div class='invalid-tooltip'>Attach a file to continue.</div>
</div>
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Dynamic and JavaScript-driven validation

Example 27: AJAX submit that maps server errors to fields (Bootstrap v5.3)

<form id='f27' novalidate>
  <input type='email' class='form-control' name='email' id='email27' aria-describedby='email27-error' required>
  <div id='email27-error' class='invalid-feedback'></div>
  <button type='submit' class='btn btn-primary mt-3'>Sign up</button>
</form>
<script>
const form27 = document.getElementById('f27');
form27.addEventListener('submit', function (event) {
  event.preventDefault();
  if (!form27.checkValidity()) {
    form27.classList.add('was-validated');
    return;
  }
  fetch('/api/signup', { method: 'POST', body: new FormData(form27) })
    .then(function (res) { return res.json(); })
    .then(function (data) {
      if (data.errors) {
        Object.keys(data.errors).forEach(function (name) {
          const field = form27.elements[name];
          const message = document.getElementById(name + '27-error') || document.getElementById(name + '-error');
          if (field && message) {
            field.classList.add('is-invalid');
            message.textContent = data.errors[name];
          }
        });
      } else {
        form27.classList.remove('was-validated');
        form27.reset();
      }
    });
});
</script>

Replace /api/signup with your own endpoint. The handler expects the server to return JSON with an errors object keyed by field name.

Example 28: Resetting the form after an AJAX submission (Bootstrap v5.3)

<script>
function resetAppearance(form) {
  form.classList.remove('was-validated');
  form.reset();
}
</script>

Call resetAppearance(form) after a successful response. Bootstrap’s guidance is to remove .was-validated when you return the form to its unvalidated appearance.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example 29: Validation on blur (Bootstrap v5.3)

<input type='email' class='form-control' id='b29' required>
<div class='invalid-feedback'>Enter a valid email address.</div>
<script>
const field29 = document.getElementById('b29');
field29.addEventListener('blur', function () {
  const valid = field29.checkValidity() && field29.value !== '';
  field29.classList.toggle('is-valid', valid);
  field29.classList.toggle('is-invalid', !valid);
});
</script>

The .is-valid and .is-invalid classes apply here without .was-validated, so the state appears as soon as the reader leaves the field.

Example 30: Password confirmation with setCustomValidity (Bootstrap v5.3)

<input type='password' class='form-control' id='pw30' minlength='8' required>
<input type='password' class='form-control mt-2' id='pc30' required>
<div class='invalid-feedback'>Passwords do not match.</div>
<script>
const pw30 = document.getElementById('pw30');
const pc30 = document.getElementById('pc30');
function checkMatch() {
  pc30.setCustomValidity(pc30.value === pw30.value ? '' : 'Passwords do not match.');
}
pw30.addEventListener('input', checkMatch);
pc30.addEventListener('input', checkMatch);
</script>

Example 31: Asynchronous username availability check (Bootstrap v5.3)

<input type='text' class='form-control' id='u31' required>
<div class='invalid-feedback'>That username is taken. Choose another.</div>
<script>
const user31 = document.getElementById('u31');
user31.addEventListener('blur', function () {
  fetch('/api/check-username?name=' + encodeURIComponent(user31.value))
    .then(function (res) { return res.json(); })
    .then(function (data) {
      user31.setCustomValidity(data.taken ? 'Taken' : '');
      user31.classList.toggle('is-invalid', data.taken);
    });
});
</script>

The availability endpoint is a placeholder path that you replace with your own. Confirm availability on the server again at submit time, because a check on blur can be stale by the time the form is sent.

Example 32: Validating rows added at runtime (Bootstrap v5.3)

<form id='f32' class='needs-validation' novalidate>
  <div id='rows32'>
    <div class='row-item mb-2'>
      <input type='text' class='form-control' name='item' required>
      <div class='invalid-feedback'>Enter an item name.</div>
    </div>
  </div>
  <button type='button' id='add32' class='btn btn-outline-secondary'>Add row</button>
  <button type='submit' class='btn btn-primary'>Save</button>
</form>
<script>
document.getElementById('add32').addEventListener('click', function () {
  const list = document.getElementById('rows32');
  const row = list.querySelector('.row-item').cloneNode(true);
  row.querySelectorAll('input').forEach(function (input) {
    input.value = '';
    input.classList.remove('is-invalid');
  });
  list.appendChild(row);
});
</script>

Because the new rows are part of the form, the form-level checkValidity() call in Example 2 covers them without extra code.

Layouts and multi-step forms

Example 33: Two-column horizontal layout (Bootstrap v5.3)

<form class='row g-3' novalidate>
  <div class='col-md-6'>
    <label for='fn33' class='form-label'>First name</label>
    <input type='text' class='form-control' id='fn33' required>
    <div class='invalid-feedback'>Enter a first name.</div>
  </div>
  <div class='col-md-6'>
    <label for='ln33' class='form-label'>Last name</label>
    <input type='text' class='form-control' id='ln33' required>
    <div class='invalid-feedback'>Enter a last name.</div>
  </div>
</form>

Example 34: Floating label with validation (Bootstrap v5.3)

<div class='form-floating'>
  <input type='email' class='form-control' id='fl34' placeholder='name@example.com' required>
  <label for='fl34'>Email address</label>
  <div class='invalid-feedback'>Enter a valid email address.</div>
</div>

Example 35: Multi-step form that checks each step before advancing (Bootstrap v5.3)

<div class='step'>
  <input type='text' class='form-control' required>
  <div class='invalid-feedback'>Complete this step.</div>
</div>
<div class='step d-none'>
  <select class='form-select' required>
    <option value=''>Choose...</option>
    <option value='a'>Option A</option>
  </select>
  <div class='invalid-feedback'>Select an option.</div>
</div>
<button type='button' id='next35' class='btn btn-primary'>Next</button>
<script>
const steps35 = document.querySelectorAll('.step');
let current35 = 0;
document.getElementById('next35').addEventListener('click', function () {
  const fields = steps35[current35].querySelectorAll('input, select, textarea');
  const ok = [...fields].every(function (field) { return field.checkValidity(); });
  if (!ok) {
    steps35[current35].classList.add('was-validated');
    return;
  }
  steps35[current35].classList.add('d-none');
  current35 += 1;
  if (steps35[current35]) {
    steps35[current35].classList.remove('d-none');
  }
});
</script>

Adding .was-validated to the step container, rather than the whole form, limits the visible feedback to the step the reader is completing.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.