Free tools Windows power users keep installed
One-click scans. No signup required.
<p>Bootstrap 5.3 styles validation states, but it does not decide whether a field is valid. That job belongs to the browser’s constraint validation (the attributes such as required, type, pattern and min), to your own JavaScript, or to your server. Once you know which of those three owns the rule, the markup and the feedback follow.</p>n<p>Below are 35 working examples grouped by behavior and control type. Every snippet is labelled Bootstrap 5.3 and uses only Bootstrap 5.3 class names; no examples from older major versions appear. The Validation page of the Bootstrap v5.3 documentation identifies v5.3.8 as the documented release. The number 35 is this article’s own curated count. Bootstrap does not publish a 35-example collection.</p>
<h2>Three ways validity reaches the screen</h2>n<p>Every example in this guide uses one of three approaches. The table compares them so you can pick a starting point before you read the code.</p>n<table>n<tr><th>Approach</th><th>Where validity comes from</th><th>How feedback appears</th><th>When users see the state</th><th>Best fit</th></tr>n<tr><td>Native browser validation</td><td>HTML attributes such as <code>required</code>, <code>type</code>, <code>pattern</code>, <code>min</code></td><td>Browser-drawn message; wording and appearance vary by browser and operating system</td><td>On submit, with no extra CSS or JavaScript</td><td>Simple forms where the default message is acceptable</td></tr>n<tr><td>Bootstrap custom client-side</td><td>The same HTML constraints, checked by <code>form.checkValidity()</code></td><td><code>.valid-feedback</code> and <code>.invalid-feedback</code>, or tooltip variants</td><td>After <code>.was-validated</code> is added to the form</td><td>Styled messages without a server round trip; needs a small script</td></tr>n<tr><td>Server-rendered</td><td>Your server’s response, including rules that need a database</td><td><code>.is-invalid</code> or <code>.is-valid</code> on the control, with feedback beside it</td><td>When the page is rendered with the result; no <code>.was-validated</code> parent needed</td><td>Checks such as “email already registered”</td></tr>n</table>n<p>Bootstrap’s documentation describes <code>.is-invalid</code> and <code>.is-valid</code> as fallback classes that do not require a <code>.was-validated</code> ancestor. That is why server-returned states work on their own. The HTML Standard remains the reference for how browsers treat form controls, so when a rule behaves unexpectedly, check the attribute against the specification first.</p>n<h2>Accessibility limit of custom client-side styles</h2>n<p>The Bootstrap validation 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 the default browser validation method while the issue remains.</p>n<p>Treat the custom client-side pattern (examples 4 to 9 and 33 to 35) as a visual enhancement. Each example below includes the error text as real content, so the meaning never depends on color alone. Where a control has a visible error, connect that text to the field with <code>aria-describedby</code>, as the server-side examples show.</p>n<h2>Foundations: native and starter validation</h2>n<h3>1. Required field with native browser feedback (Bootstrap 5.3)</h3>n<p>No Bootstrap JavaScript is needed. The browser blocks submission and shows its own message.</p>n<pre><code><form>n <label for=’name’ class=’form-label’>Name</label>n <input type=’text’ class=’form-control’ id=’name’ required>n <button class=’btn btn-primary’ type=’submit’>Submit</button>n</form></code></pre>n<h3>2. Email and number types (Bootstrap 5.3)</h3>n<p>Bootstrap’s forms guidance recommends appropriate input types such as <code>email</code> and <code>number</code>, which let the browser supply relevant controls and checks.</p>n<pre><code><input type=’email’ class=’form-control’ id=’mail’ required>n<input type=’number’ class=’form-control’ id=’seats’ min=’1′ max=’10’ step=’1′ required></code></pre>n<h3>3. Pattern and minimum length (Bootstrap 5.3)</h3>n<p>The <code>title≶attribute is shown as a hint by many browsers when the pattern fails, but its display is not uniform across browsers.</p>n<pre><code><input type=’text’ class=’form-control’ id=’code’ pattern='[A-Za-z]{3,}’ minlength=’3′ title=’Three or more letters’ required></code></pre>n<h3>4. Starter custom validation with novalidate (Bootstrap 5.3)</h3>n<p>This is the documented starter pattern. The <code>novalidate</code> attribute suppresses the browser’s own bubbles while the script still uses the constraint API. The script blocks invalid submissions and then adds <code>.was-validated</code>, which activates Bootstrap’s styles. It is a starting point, not a replacement for server-side validation.</p>n<pre><code><form class=’row g-3 needs-validation’ novalidate>n <div class=’col-md-6’>n <label for=’firstName’ class=’form-label’>First name</label>n <input type=’text’ class=’form-control’ id=’firstName’ required>n <div class=’valid-feedback’>Looks good!</div>n <div class=’invalid-feedback’>Please enter a first name.</div>n </div>n <div class=’col-12’>n <button class=’btn btn-primary’ type=’submit’>Submit form</button>n </div>n</form>n<script>n(() => {n ‘use strict’n const forms = document.querySelectorAll(‘.needs-validation’)n Array.from(forms).forEach(form => {n form.addEventListener(‘submit’, event => {n if (!form.checkValidity()) {n event.preventDefault()n event.stopPropagation()n }n form.classList.add(‘was-validated’)n }, false)n })n})()n</script></code></pre>n<h3>5. Returning the form to its unvalidated look after AJAX (Bootstrap 5.3)</h3>n<p>Bootstrap’s guidance is to remove <code>.was-validated</code> when you reset the form’s appearance after a dynamic submission. Use this script in place of example 4’s script on the same form, because both would bind a submit handler. The endpoint is a placeholder for your own API.</p>n<pre><code>(() => {n const form = document.querySelector(‘.needs-validation’)n form.addEventListener(‘submit’, async event => {n event.preventDefault()n if (!form.checkValidity()) {n form.classList.add(‘was-validated’)n returnn }n const response = await fetch(‘/api/subscribe’, { method: ‘POST’, body: new FormData(form) })n if (response.ok) {n form.reset()n form.classList.remove(‘was-validated’)n }n })n})()</code></pre>n<h2>Feedback placement</h2>n<p>Bootstrap’s selectors show feedback only when the feedback element is a later sibling of the control. Put the message immediately after the input, inside the same wrapper, and the examples below will behave as described.</p>n<h3>6. Valid and invalid inline feedback (Bootstrap 5.3)</h3>n<p>Both messages sit below the control. Whichever applies is shown once <code>.was-validated</code> is on the form.</p>n<pre><code><div class=’col-md-4’>n <label for=’zip’ class=’form-label’>ZIP code</label>n <input type=’text’ class=’form-control’ id=’zip’ pattern='[0-9]{5}’ required>n <div class=’valid-feedback’>Valid ZIP code.</div>n <div class=’invalid-feedback’>Enter five digits.</div>n</div></code></pre>n<h3>7. Success message only (Bootstrap 5.3)</h3>n<p>Define only <code>.valid-feedback</code> when a confirmation is useful and the invalid state needs no extra text. The invalid border still appears.</p>n<pre><code><input type=’text’ class=’form-control’ id=’coupon’ required>n<div class=’valid-feedback’>Coupon applied.</div></code></pre>n<h3>8. Specific invalid message (Bootstrap 5.3)</h3>n<p>When a generic message such as “Invalid” would not help, write the rule into the text.</p>n<pre><code><input type=’text’ class=’form-control’ id=’postcode’ pattern='[A-Z]{1,2}[0-9][0-9A-Z]? [0-9][A-Z]{2}’ required>n<div class=’invalid-feedback’>Use the format SW1A 1AA.</div></code></pre>n<h3>9. Tooltip feedback (Bootstrap 5.3)</h3>n<p>Tooltip variants use <code>.valid-tooltip</code> or <code>.invalid-tooltip</code>. The parent must be relatively positioned, so add <code>.position-relative</code> to it. The accessibility limit described earlier applies to this pattern, so keep the same message available in the page’s text where a user needs it.</p>n<pre><code><div class=’col-md-4 position-relative’>n <label for=’city’ class=’form-label’>City</label>n <input type=’text’ class=’form-control’ id=’city’ required>n <div class=’invalid-tooltip’>Enter a city.</div>n</div></code></pre>n<h2>Server-side states</h2>n<p>These examples assume your server has checked the submission and rendered the result. The classes do not depend on <code>.was-validated</code>.</p>n<h3>10. One field rejected by the server (Bootstrap 5.3)</h3>n<p>The <code>aria-describedby</code> attribute points to the error message’s <code>id</code>, which connects the text to the field.</p>n<pre><code><label for=’email’ class=’form-label’>Email</label>n<input type=’email’ class=’form-control is-invalid’ id=’email’ name=’email’ value=’[email protected]’ aria-describedby=’emailError’ required>n<div id=’emailError’ class=’invalid-feedback’>This email address is already registered.</div></code></pre>n<h3>11. Field accepted by the server (Bootstrap 5.3)</h3>n<pre><code><input type=’text’ class=’form-control is-valid’ id=’handle’ name=’handle’ value=’ada_lovelace’ aria-describedby=’handleOk’ required>n<div id=’handleOk’ class=’valid-feedback’>This username is available.</div></code></pre>n<h3>12. Several fields rejected together (Bootstrap 5.3)</h3>n<p>Give each field its own message so screen readers announce the right text when a user moves to that field.</p>n<pre><code><input type=’text’ class=’form-control is-invalid’ id=’first’ aria-describedby=’firstError’ required>n<div id=’firstError’ class=’invalid-feedback’>First name is required.</div>n<input type=’text’ class=’form-control is-invalid’ id=’last’ aria-describedby=’lastError’ required>n<div id=’lastError’ class=’invalid-feedback’>Last name is too short.</div></code></pre>n<h3>13. Applying a JSON error map in the browser (Bootstrap 5.3)</h3>n<p>When your API returns field-level errors, map them onto the matching controls. This assumes each control has an error element whose id is the field name followed by <code>Error</code>.</p>n<pre><code>function showServerErrors(form, errors) {n for (const [name, message] of Object.entries(errors)) {n const input = form.elements[name]n const box = document.getElementById(name + ‘Error’)n input.classList.add(‘is-invalid’)n input.setAttribute(‘aria-describedby’, box.id)n box.textContent = messagen }n}nshowServerErrors(document.querySelector(‘form’), { email: ‘Email already registered.’ })</code></pre>n<h2>Text, email, password and textarea</h2>n<h3>14. Full name with autocomplete (Bootstrap 5.3)</h3>n<pre><code><input type=’text’ class=’form-control’ id=’fullName’ autocomplete=’name’ required>n<div class=’invalid-feedback’>Enter your full name.</div></code></pre>n<h3>15. Work email restricted to one domain (Bootstrap 5.3)</h3>n<p>The browser checks the email format. The script adds a domain rule using <code>setCustomValidity()</code>, which the constraint API reads like any other error. An empty string clears it.</p>n<pre><code><input type=’email’ class=’form-control’ id=’workEmail’ required>n<div class=’invalid-feedback’>Use your work address ending in @example.com.</div>n<script>nconst workEmail = document.getElementById(‘workEmail’)nworkEmail.addEventListener(‘input’, () => {n const ok = workEmail.value.endsWith(‘@example.com’) || workEmail.value === ”n workEmail.setCustomValidity(ok ? ” : ‘Domain not allowed.’)n})n</script></code></pre>n<h3>16. Password confirmation (Bootstrap 5.3)</h3>n<pre><code><input type=’password’ class=’form-control’ id=’password’ minlength=’8′ required>n<input type=’password’ class=’form-control’ id=’confirm’ required>n<div class=’invalid-feedback’>Passwords must match.</div>n<script>nconst pw = document.getElementById(‘password’)nconst confirm = document.getElementById(‘confirm’)nconfirm.addEventListener(‘input’, () => {n confirm.setCustomValidity(confirm.value === pw.value ? ” : ‘Passwords must match.’)n})n</script></code></pre>n<h3>17. Phone number with a pattern (Bootstrap 5.3)</h3>n<pre><code><input type=’tel’ class=’form-control’ id=’phone’ pattern='[0-9+ ()-]{7,20}’ autocomplete=’tel’ required>n<div class=’invalid-feedback’>Use 7 to 20 digits, spaces, +, ( ) or -.</div></code></pre>n<h3>18. Textarea with minimum length (Bootstrap 5.3)</h3>n<pre><code><label for=’message’ class=’form-label’>Message</label>n<textarea class=’form-control’ id=’message’ rows=’4′ minlength=’20’ required></textarea>n<div class=’invalid-feedback’>Write at least 20 characters.</div></code></pre>n<h2>Select controls</h2>n<h3>19. Placeholder option that must be replaced (Bootstrap 5.3)</h3>n<p>Use <code>.form-select</code> on the select. The placeholder has an empty value, so <code>required</code> rejects it.</p>n<pre><code><select class=’form-select’ id=’plan’ required>n <option value=”>Choose a plan</option>n <option value=’basic’>Basic</option>n <option value=’pro’>Pro</option>n</select>n<div class=’invalid-feedback’>Select a plan.</div></code></pre>n<h3>20. Confirmation after a valid choice (Bootstrap 5.3)</h3>n<pre><code><select class=’form-select’ id=’size’ required>n <option value=”>Choose a size</option>n <option value=’s’>Small</option>n <option value=’m’>Medium</option>n</select>n<div class=’valid-feedback’>Size selected.</div>n<div class=’invalid-feedback’>Select a size.</div></code></pre>n<h3>21. Dependent select enabled by a choice (Bootstrap 5.3)</h3>n<p>Disabled controls are not validated, so the second select stays out of the check until the first has a value.</p>n<pre><code><select class=’form-select’ id=’country’ required> … </select>n<select class=’form-select’ id=’region’ disabled> … </select>n<script>nconst country = document.getElementById(‘country’)nconst region = document.getElementById(‘region’)ncountry.addEventListener(‘change’, () => {n region.disabled = country.value === ”n region.required = truen})n</script></code></pre>n<h2>Checkboxes</h2>n<h3>22. Single required checkbox (Bootstrap 5.3)</h3>n<pre><code><div class=’form-check’>n <input class=’form-check-input’ type=’checkbox’ id=’terms’ required>n <label class=’form-check-label’ for=’terms’>I agree to the terms</label>n <div class=’invalid-feedback’>You must agree before submitting.</div>n</div></code></pre>n<h3>23. At least one of a group of checkboxes (Bootstrap 5.3)</h3>n<p>No single checkbox is required here. The script sets the same custom error on every box until one is checked.</p>n<pre><code><input class=’form-check-input’ type=’checkbox’ name=’topic’ id=’t1’>n<input class=’form-check-input’ type=’checkbox’ name=’topic’ id=’t2’>n<div class=’invalid-feedback d-block’>Select at least one topic.</div>n<script>nconst boxes = document.querySelectorAll(“input[name=’topic’]”)nfunction syncTopics() {n const any = Array.from(boxes).some(b => b.checked)n boxes.forEach(b => b.setCustomValidity(any ? ” : ‘Select at least one topic.’))n}nboxes.forEach(b => b.addEventListener(‘change’, syncTopics))nsyncTopics()n</script></code></pre>n<h3>24. Checkbox rejected by the server (Bootstrap 5.3)</h3>n<pre><code><div class=’form-check’>n <input class=’form-check-input is-invalid’ type=’checkbox’ id=’newsletter’ aria-describedby=’nlError’>n <label class=’form-check-label’ for=’newsletter’>Send me updates</label>n <div id=’nlError’ class=’invalid-feedback’>This option is unavailable for your region.</div>n</div></code></pre>n<h2>Radio buttons</h2>n<h3>25. Required radio group (Bootstrap 5.3)</h3>n<p>Radios with the same <code>name</code> form one group. Mark one radio <code>required</code>, and place the feedback after the last radio so the sibling selector applies.</p>n<pre><code><div class=’form-check’>n <input class=’form-check-input’ type=’radio’ name=’shipping’ id=’ship1′ required>n <label class=’form-check-label’ for=’ship1’>Standard</label>n</div>n<div class=’form-check’>n <input class=’form-check-input’ type=’radio’ name=’shipping’ id=’ship2’>n <label class=’form-check-label’ for=’ship2’>Express</label>n <div class=’invalid-feedback’>Choose a shipping method.</div>n</div></code></pre>n<h3>26. Server-rejected radio group (Bootstrap 5.3)</h3>n<p>Each radio references the same error message, so assistive technology reads the message whichever option has focus.</p>n<pre><code><input class=’form-check-input is-invalid’ type=’radio’ name=’payment’ id=’pay1′ aria-describedby=’payError’>n<label class=’form-check-label’ for=’pay1’>Card</label>n<input class=’form-check-input is-invalid’ type=’radio’ name=’payment’ id=’pay2′ aria-describedby=’payError’>n<label class=’form-check-label’ for=’pay2’>Bank transfer</label>n<div id=’payError’ class=’invalid-feedback d-block’>This payment method is temporarily unavailable.</div></code></pre>n<h2>File inputs</h2>n<h3>27. Required file upload (Bootstrap 5.3)</h3>n<pre><code><label for=’resume’ class=’form-label’>Resume (PDF)</label>n<input class=’form-control’ type=’file’ id=’resume’ accept=’.pdf’ required>n<div class=’invalid-feedback’>Upload a PDF file.</div></code></pre>n<h3>28. File size limit with a script (Bootstrap 5.3)</h3>n<p>The <code>accept</code> attribute only filters the file picker; it does not enforce type or size. The size check below runs on selection.</p>n<pre><code><input class=’form-control’ type=’file’ id=’photo’ accept=’image/*’ required>n<div class=’invalid-feedback’>The image must be 2 MB or smaller.</div>n<script>nconst photo = document.getElementById(‘photo’)nphoto.addEventListener(‘change’, () => {n const file = photo.files[0]n photo.setCustomValidity(file && file.size > 2 * 1024 * 1024 ? ‘Too large.’ : ”)n})n</script></code></pre>n<h3>29. Maximum number of files (Bootstrap 5.3)</h3>n<pre><code><input class=’form-control’ type=’file’ id=’attachments’ multiple required>n<div class=’invalid-feedback’>Attach between 1 and 3 files.</div>n<script>nconst attachments = document.getElementById(‘attachments’)nattachments.addEventListener(‘change’, () => {n const n = attachments.files.lengthn attachments.setCustomValidity(n >= 1 && n <= 3 ? ” : ‘Too many files.’)n})n</script></code></pre>n<h2>Input groups</h2>n<h3>30. Input group with has-validation (Bootstrap 5.3)</h3>n<p>The <code>.has-validation</code> class on the input group handles border-radius when the feedback is part of the group.</p>n<pre><code><div class=’input-group has-validation’>n <span class=’input-group-text’ id=’at’>@</span>n <input type=’text’ class=’form-control’ aria-describedby=’at’ required>n <div class=’invalid-feedback’>Choose a username.</div>n</div></code></pre>n<h3>31. Server-rejected input group (Bootstrap 5.3)</h3>n<pre><code><div class=’input-group has-validation’>n <span class=’input-group-text’>https://</span>n <input type=’text’ class=’form-control is-invalid’ id=’site’ aria-describedby=’siteError’ required>n <div id=’siteError’ class=’invalid-feedback’>This domain is already in use.</div>n</div></code></pre>n<h3>32. Currency select and amount (Bootstrap 5.3)</h3>n<pre><code><div class=’input-group has-validation’>n <select class=’form-select’ id=’currency’ required>n <option value=”>Currency</option>n <option value=’USD’>USD</option>n <option value=’EUR’>EUR</option>n </select>n <input type=’number’ class=’form-control’ min=’1′ step=’0.01′ required>n <div class=’invalid-feedback’>Choose a currency and enter an amount of at least 1.</div>n</div></code></pre>n<h2>Custom rules and live feedback</h2>n<h3>33. Format message with reportValidity() (Bootstrap 5.3)</h3>n<p>The script sets a message when the format is wrong. Calling <code>reportValidity()</code> on submit shows the browser’s bubble with that text. This shows the browser’s own message, so it sits outside the Bootstrap styling.</p>n<pre><code><input type=’text’ class=’form-control’ id=’code’ required>n<script>nconst code = document.getElementById(‘code’)ncode.addEventListener(‘input’, () => {n code.setCustomValidity(/^[A-Z]{3}-\d{4}$/.test(code.value) || code.value === ” ? ” : ‘Use the format ABC-1234.’)n})n// On submit: if (!code.checkValidity()) code.reportValidity()n</script></code></pre>n<h3>34. Live state after a field loses focus (Bootstrap 5.3)</h3>n<p>Feedback waits until the user leaves the field. After that, it updates as they type. Use this only with the invalid feedback siblings from earlier examples.</p>n<pre><code>document.querySelectorAll(‘.form-control[required]’).forEach(field => {n field.addEventListener(‘blur’, () => {n field.classList.toggle(‘is-invalid’, !field.checkValidity())n })n field.addEventListener(‘input’, () => {n if (field.classList.contains(‘is-invalid’)) {n field.classList.toggle(‘is-invalid’, !field.checkValidity())n }n })n})</code></pre>n<h3>35. Submit handler that focuses the first invalid field (Bootstrap 5.3)</h3>n<p>Moving focus to the first error helps keyboard and screen reader users find the problem. Pair it with the feedback messages from example 6.</p>n<pre><code>const form = document.querySelector(‘.needs-validation’)nform.addEventListener(‘submit’, event => {n if (!form.checkValidity()) {n event.preventDefault()n form.classList.add(‘was-validated’)n form.querySelector(‘:invalid’).focus()n }n})</code></pre>n<h2>Why the feedback does not appear</h2>n<ul>n<li><strong>The message never shows.</strong> The feedback element is not a later sibling of the control. Move it immediately after the input, inside the same wrapper.</li>n<li><strong>Browser bubbles and Bootstrap messages both appear.</strong> The form is missing <code>novalidate</code> while the custom script is active.</li>n<li><strong>Green or red states appear before the user submits.</strong> <code>.was-validated</code> was added too early, or a server class such as <code>.is-invalid</code> was rendered by mistake.</li>n<li><strong>The tooltip is misplaced.</strong> The parent is not positioned. Add <code>.position-relative</code>.</li>n<li><strong>The form looks validated after an AJAX reset.</strong> Remove <code>.was-validated</code> as shown in example 5.</li>n<li><strong>A disabled field is never rejected.</strong> Browsers skip disabled controls during constraint validation, which is why example 21 enables the second select only after a choice.</li>n</ul>n<h2>Choosing a starting point</h2>n<ol>n<li>If the browser’s default messages are acceptable, start with examples 1 to 3.</li>n<li>If you want styled messages without a server round trip, start with example 4, then add feedback from examples 6 to 8.</li>n<li>If the rule depends on your data, render the state on the server with examples 10 to 13.</li>n<li>For control-specific rules, pick the matching section: text (14 to 18), select (19 to 21), checkbox (22 to 24), radio (25 to 26), file (27 to 29), or input group (30 to 32).</li>n</ol>
Quick Recap
Best Value
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
Rank #3
#1 Best Overall
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
As an Amazon Associate I earn from qualifying purchases.
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.




