]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Add validation feedback IDs for a11y, set aria-invalid on controls on form submission...
authorMark Otto <markd.otto@gmail.com>
Tue, 14 Apr 2026 22:59:17 +0000 (17:59 -0500)
committerGitHub <noreply@github.com>
Tue, 14 Apr 2026 22:59:17 +0000 (17:59 -0500)
site/src/content/docs/forms/validation.mdx
site/static/docs/[version]/assets/js/validate-forms.js

index 12b169589f2b4d1141e019d2e63e3981beb1b638..72423fdcaa5482be66491421b244b0a30f8be40b 100644 (file)
@@ -31,49 +31,51 @@ For custom Bootstrap form validation messages, add `data-bs-validate` and the `n
 
 Use `data-bs-validate="valid"` to also show success styling on valid fields. Add `required` to each form input and `.invalid-feedback` to provide field-specific error messages. If you enable valid styling, use `.valid-feedback` for success messages.
 
+**For accessibility,** add `aria-describedby` to each form control, pointing to the `id` of its feedback message. This ensures screen readers announce the error when the user focuses an invalid field. The example JavaScript below also sets `aria-invalid` on each control at submit time and clears it as users correct their input.
+
 For example, try to submit the form below; our JavaScript will intercept the submit button and relay feedback to you.
 
 <Example code={`<form class="grid grid-cols-1 gap-5" data-bs-validate novalidate><!-- [!code highlight] -->
     <div class="grid gap-5">
       <div class="form-field md:g-col-6">
         <label for="validationCustom01" class="form-label">Name</label>
-        <input type="text" class="form-control" id="validationCustom01" value="Mark Otto" required><!-- [!code highlight] -->
-        <div class="invalid-feedback">Full name is required</div><!-- [!code highlight] -->
+        <input type="text" class="form-control" id="validationCustom01" value="Mark Otto" required aria-describedby="validationCustom01Feedback"><!-- [!code highlight] -->
+        <div id="validationCustom01Feedback" class="invalid-feedback">Full name is required</div><!-- [!code highlight] -->
       </div>
       <div class="form-field md:g-col-6">
         <label for="validationCustomUsername" class="form-label">Donation amount</label>
         <div class="input-group">
           <span class="input-group-text" id="inputGroupPrepend">$</span>
-          <input type="number" class="form-control" value="100" id="validationCustomUsername" aria-describedby="inputGroupPrepend" required><!-- [!code highlight] -->
+          <input type="number" class="form-control" value="100" id="validationCustomUsername" aria-describedby="inputGroupPrepend validationCustomUsernameFeedback" required><!-- [!code highlight] -->
         </div>
-        <div class="invalid-feedback">Please enter a donation amount</div><!-- [!code highlight] -->
+        <div id="validationCustomUsernameFeedback" class="invalid-feedback">Please enter a donation amount</div><!-- [!code highlight] -->
       </div>
     </div>
 
     <div class="form-field">
       <label for="validationCustomAddress" class="form-label">Address</label>
-      <input type="text" class="form-control" id="validationCustomAddress" value="1234 Main St" required><!-- [!code highlight] -->
-      <div class="invalid-feedback">Please enter a mailing address</div><!-- [!code highlight] -->
+      <input type="text" class="form-control" id="validationCustomAddress" value="1234 Main St" required aria-describedby="validationCustomAddressFeedback"><!-- [!code highlight] -->
+      <div id="validationCustomAddressFeedback" class="invalid-feedback">Please enter a mailing address</div><!-- [!code highlight] -->
     </div>
 
     <div class="grid gap-5">
       <div class="form-field md:g-col-6">
         <label for="validationCustom03" class="form-label">City</label>
-        <input type="text" class="form-control" id="validationCustom03" required><!-- [!code highlight] -->
-        <div class="invalid-feedback">Please provide a city</div><!-- [!code highlight] -->
+        <input type="text" class="form-control" id="validationCustom03" required aria-describedby="validationCustom03Feedback"><!-- [!code highlight] -->
+        <div id="validationCustom03Feedback" class="invalid-feedback">Please provide a city</div><!-- [!code highlight] -->
       </div>
       <div class="form-field md:g-col-4">
         <label for="validationCustom04" class="form-label">State</label>
-        <select class="form-control" id="validationCustom04" required><!-- [!code highlight] -->
+        <select class="form-control" id="validationCustom04" required aria-describedby="validationCustom04Feedback"><!-- [!code highlight] -->
           <option selected disabled value="">Choose…</option>
           <option>...</option>
         </select>
-        <div class="invalid-feedback">Please select a state</div><!-- [!code highlight] -->
+        <div id="validationCustom04Feedback" class="invalid-feedback">Please select a state</div><!-- [!code highlight] -->
       </div>
       <div class="form-field md:g-col-2">
         <label for="validationCustom05" class="form-label">Zip</label>
-        <input type="text" class="form-control" id="validationCustom05" required><!-- [!code highlight] -->
-        <div class="invalid-feedback">Required</div><!-- [!code highlight] -->
+        <input type="text" class="form-control" id="validationCustom05" required aria-describedby="validationCustom05Feedback"><!-- [!code highlight] -->
+        <div id="validationCustom05Feedback" class="invalid-feedback">Required</div><!-- [!code highlight] -->
       </div>
     </div>
 
@@ -189,9 +191,9 @@ For invalid fields, ensure that the invalid feedback/error message is associated
     <div class="grid gap-5">
       <div class="form-field md:g-col-6">
         <label for="validationServer01" class="form-label">Name</label>
-        <input type="text" class="form-control is-valid" id="validationServer01" value="Mark Otto"><!-- [!code highlight] -->
-        <div class="valid-feedback">Looks great!</div><!-- [!code highlight] -->
-        <div class="invalid-feedback">Full name is required</div><!-- [!code highlight] -->
+        <input type="text" class="form-control is-valid" id="validationServer01" value="Mark Otto" aria-describedby="validationServer01Valid validationServer01Invalid"><!-- [!code highlight] -->
+        <div id="validationServer01Valid" class="valid-feedback">Looks great!</div><!-- [!code highlight] -->
+        <div id="validationServer01Invalid" class="invalid-feedback">Full name is required</div><!-- [!code highlight] -->
       </div>
       <div class="form-field md:g-col-6">
         <label for="validationServerDonation" class="form-label">Donation amount</label>
@@ -206,9 +208,9 @@ For invalid fields, ensure that the invalid feedback/error message is associated
 
     <div class="form-field">
       <label for="validationServerAddress" class="form-label">Address</label>
-      <input type="text" class="form-control is-valid" id="validationServerAddress" value="1234 Main St"><!-- [!code highlight] -->
-      <div class="valid-feedback">Looks great!</div><!-- [!code highlight] -->
-      <div class="invalid-feedback">Please enter a mailing address</div>
+      <input type="text" class="form-control is-valid" id="validationServerAddress" value="1234 Main St" aria-describedby="validationServerAddressValid validationServerAddressInvalid"><!-- [!code highlight] -->
+      <div id="validationServerAddressValid" class="valid-feedback">Looks great!</div><!-- [!code highlight] -->
+      <div id="validationServerAddressInvalid" class="invalid-feedback">Please enter a mailing address</div>
     </div>
 
     <div class="grid gap-5">
@@ -279,8 +281,8 @@ Validation styles are available for the following form controls and components:
 <Example code={`<form class="vstack gap-5">
     <div class="form-field">
       <label for="validationTextarea" class="form-label">Textarea</label>
-      <textarea class="form-control is-invalid" id="validationTextarea" placeholder="Required example textarea" required></textarea>
-      <div class="invalid-feedback">
+      <textarea class="form-control is-invalid" id="validationTextarea" placeholder="Required example textarea" required aria-describedby="validationTextareaFeedback"></textarea>
+      <div id="validationTextareaFeedback" class="invalid-feedback">
         Please enter a message in the textarea.
       </div>
     </div>
@@ -289,7 +291,7 @@ Validation styles are available for the following form controls and components:
       <label class="form-label">Checkbox</label>
       <div class="form-field">
         <div class="check">
-          <input type="checkbox" class="is-invalid" id="validationFormCheck1" required>
+          <input type="checkbox" class="is-invalid" id="validationFormCheck1" required aria-describedby="validationFormCheck1Feedback">
           <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'>
             <path class="checked" fill='none' stroke='currentcolor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m4 8 3 3 5-5'/>
             <path class="indeterminate" fill='none' stroke='currentcolor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4.5 8.5h6'/>
@@ -297,7 +299,7 @@ Validation styles are available for the following form controls and components:
         </div>
         <div class="form-field-content">
           <label for="validationFormCheck1">Check this checkbox</label>
-          <div class="invalid-feedback">Example invalid feedback text</div>
+          <div id="validationFormCheck1Feedback" class="invalid-feedback">Example invalid feedback text</div>
         </div>
       </div>
     </div>
@@ -305,14 +307,14 @@ Validation styles are available for the following form controls and components:
     <div class="form-group">
       <label class="form-label">Radio</label>
       <div class="form-field">
-        <input type="radio" class="radio is-invalid" id="validationFormCheck2" name="radio-stacked" required>
+        <input type="radio" class="radio is-invalid" id="validationFormCheck2" name="radio-stacked" required aria-describedby="validationFormRadioFeedback">
         <label for="validationFormCheck2">Toggle this radio</label>
       </div>
       <div class="form-field">
-        <input type="radio" class="radio is-invalid" id="validationFormCheck3" name="radio-stacked" required>
+        <input type="radio" class="radio is-invalid" id="validationFormCheck3" name="radio-stacked" required aria-describedby="validationFormRadioFeedback">
         <div class="form-field-content">
           <label for="validationFormCheck3">Or toggle this other radio</label>
-          <div class="invalid-feedback">More example invalid feedback text</div>
+          <div id="validationFormRadioFeedback" class="invalid-feedback">More example invalid feedback text</div>
         </div>
       </div>
     </div>
@@ -321,44 +323,44 @@ Validation styles are available for the following form controls and components:
       <label class="form-label">Switch</label>
       <div class="form-field">
         <div class="switch">
-          <input type="checkbox" id="validationFormSwitch" role="switch" switch class="is-invalid">
+          <input type="checkbox" id="validationFormSwitch" role="switch" switch class="is-invalid" aria-describedby="validationFormSwitchFeedback">
         </div>
         <div class="form-field-content">
           <label for="validationFormSwitch">Toggle this switch</label>
-          <div class="invalid-feedback">Example invalid switch feedback</div>
+          <div id="validationFormSwitchFeedback" class="invalid-feedback">Example invalid switch feedback</div>
         </div>
       </div>
     </div>
 
     <div class="form-field">
       <label for="validationSelect" class="form-label">Select</label>
-      <select class="form-control is-invalid" id="validationSelect" required>
+      <select class="form-control is-invalid" id="validationSelect" required aria-describedby="validationSelectFeedback">
         <option value="">Open this select menu</option>
         <option value="1">One</option>
         <option value="2">Two</option>
         <option value="3">Three</option>
       </select>
-      <div class="invalid-feedback">Example invalid select feedback</div>
+      <div id="validationSelectFeedback" class="invalid-feedback">Example invalid select feedback</div>
     </div>
 
     <div class="form-field">
       <label for="validationFile" class="form-label">File input</label>
-      <input type="file" class="form-control is-invalid" id="validationFile" required>
-      <div class="invalid-feedback">Example invalid form file feedback</div>
+      <input type="file" class="form-control is-invalid" id="validationFile" required aria-describedby="validationFileFeedback">
+      <div id="validationFileFeedback" class="invalid-feedback">Example invalid form file feedback</div>
     </div>
 
     <div class="form-field">
       <label for="validationRange" class="form-label">Range</label>
-      <input type="range" class="form-range is-invalid" id="validationRange" min="0" max="5" required>
-      <div class="invalid-feedback">Example invalid range feedback</div>
+      <input type="range" class="form-range is-invalid" id="validationRange" min="0" max="5" required aria-describedby="validationRangeFeedback">
+      <div id="validationRangeFeedback" class="invalid-feedback">Example invalid range feedback</div>
     </div>
 
     <div class="form-field">
       <div class="form-floating">
-        <input type="email" class="form-control is-invalid" id="validationFloating" placeholder="name@example.com" value="test@example.com">
+        <input type="email" class="form-control is-invalid" id="validationFloating" placeholder="name@example.com" value="test@example.com" aria-describedby="validationFloatingFeedback">
         <label for="validationFloating">Email address</label>
       </div>
-      <div class="invalid-feedback d-block">Example invalid floating label feedback</div>
+      <div id="validationFloatingFeedback" class="invalid-feedback d-block">Example invalid floating label feedback</div>
     </div>
 
     <div class="form-field">
@@ -367,27 +369,27 @@ Validation styles are available for the following form controls and components:
         <div class="form-adorn-icon">
           <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/></svg>
         </div>
-        <input type="text" class="form-ghost" id="validationAdorn" placeholder="Search...">
+        <input type="text" class="form-ghost" id="validationAdorn" placeholder="Search..." aria-describedby="validationAdornFeedback">
       </div>
-      <div class="invalid-feedback">Example invalid adorned input feedback</div>
+      <div id="validationAdornFeedback" class="invalid-feedback">Example invalid adorned input feedback</div>
     </div>
 
     <div class="form-field">
       <label for="validationInputGroup" class="form-label">Input group</label>
       <div class="input-group">
         <span class="input-group-text">@</span>
-        <input type="text" class="form-control is-invalid" id="validationInputGroup" placeholder="Username">
+        <input type="text" class="form-control is-invalid" id="validationInputGroup" placeholder="Username" aria-describedby="validationInputGroupFeedback">
       </div>
-      <div class="invalid-feedback">Please choose a username</div>
+      <div id="validationInputGroupFeedback" class="invalid-feedback">Please choose a username</div>
     </div>
 
     <div class="form-field">
       <label class="form-label" for="validationChipInput">Tags</label>
       <div class="chip-input">
         <span class="chip">Example</span>
-        <input type="text" class="form-ghost is-invalid" id="validationChipInput" placeholder="Add tag...">
+        <input type="text" class="form-ghost is-invalid" id="validationChipInput" placeholder="Add tag..." aria-describedby="validationChipInputFeedback">
       </div>
-      <div class="invalid-feedback">Please add at least one tag</div>
+      <div id="validationChipInputFeedback" class="invalid-feedback">Please add at least one tag</div>
     </div>
   </form>`} />
 
@@ -403,43 +405,43 @@ This example functions the same as the [custom styles example](#custom-styles).
     <div class="grid gap-5">
       <div class="form-field md:g-col-6">
         <label for="validationTooltip01" class="form-label">Name</label>
-        <input type="text" class="form-control" id="validationTooltip01" value="Mark Otto" required>
-        <div class="tooltip invalid-tooltip">Full name is required</div><!-- [!code highlight] -->
+        <input type="text" class="form-control" id="validationTooltip01" value="Mark Otto" required aria-describedby="validationTooltip01Feedback">
+        <div id="validationTooltip01Feedback" class="tooltip invalid-tooltip">Full name is required</div><!-- [!code highlight] -->
       </div>
       <div class="form-field md:g-col-6">
         <label for="validationTooltipDonation" class="form-label">Donation amount</label>
         <div class="input-group">
           <span class="input-group-text" id="inputGroupTooltipPrepend">$</span>
-          <input type="number" class="form-control" value="100" id="validationTooltipDonation" aria-describedby="inputGroupTooltipPrepend" required>
+          <input type="number" class="form-control" value="100" id="validationTooltipDonation" aria-describedby="inputGroupTooltipPrepend validationTooltipDonationFeedback" required>
         </div>
-        <div class="tooltip invalid-tooltip">Please enter a donation amount</div><!-- [!code highlight] -->
+        <div id="validationTooltipDonationFeedback" class="tooltip invalid-tooltip">Please enter a donation amount</div><!-- [!code highlight] -->
       </div>
     </div>
 
     <div class="form-field">
       <label for="validationTooltipAddress" class="form-label">Address</label>
-      <input type="text" class="form-control" id="validationTooltipAddress" value="1234 Main St" required>
-      <div class="tooltip invalid-tooltip">Please enter a mailing address</div><!-- [!code highlight] -->
+      <input type="text" class="form-control" id="validationTooltipAddress" value="1234 Main St" required aria-describedby="validationTooltipAddressFeedback">
+      <div id="validationTooltipAddressFeedback" class="tooltip invalid-tooltip">Please enter a mailing address</div><!-- [!code highlight] -->
     </div>
 
     <div class="grid gap-5">
       <div class="form-field md:g-col-6">
         <label for="validationTooltip03" class="form-label">City</label>
-        <input type="text" class="form-control" id="validationTooltip03" required>
-        <div class="tooltip invalid-tooltip">Please provide a city</div><!-- [!code highlight] -->
+        <input type="text" class="form-control" id="validationTooltip03" required aria-describedby="validationTooltip03Feedback">
+        <div id="validationTooltip03Feedback" class="tooltip invalid-tooltip">Please provide a city</div><!-- [!code highlight] -->
       </div>
       <div class="form-field md:g-col-4">
         <label for="validationTooltip04" class="form-label">State</label>
-        <select class="form-control" id="validationTooltip04" required>
+        <select class="form-control" id="validationTooltip04" required aria-describedby="validationTooltip04Feedback">
           <option selected disabled value="">Choose…</option>
           <option>...</option>
         </select>
-        <div class="tooltip invalid-tooltip">Please select a state</div><!-- [!code highlight] -->
+        <div id="validationTooltip04Feedback" class="tooltip invalid-tooltip">Please select a state</div><!-- [!code highlight] -->
       </div>
       <div class="form-field md:g-col-2">
         <label for="validationTooltip05" class="form-label">Zip</label>
-        <input type="text" class="form-control" id="validationTooltip05" required>
-        <div class="tooltip invalid-tooltip">Required</div><!-- [!code highlight] -->
+        <input type="text" class="form-control" id="validationTooltip05" required aria-describedby="validationTooltip05Feedback">
+        <div id="validationTooltip05Feedback" class="tooltip invalid-tooltip">Required</div><!-- [!code highlight] -->
       </div>
     </div>
 
index 846733643f62f966c3f62ef945f9742827df2be4..7fc3adf6733bc1438997023452d6d63f65dcf6f0 100644 (file)
         event.preventDefault()
         event.stopPropagation()
       }
+
+      for (const control of form.elements) {
+        if (control.willValidate) {
+          control.setAttribute('aria-invalid', String(!control.validity.valid))
+        }
+      }
+    })
+
+    // Clear aria-invalid as users correct individual fields
+    form.addEventListener('input', event => {
+      const control = event.target
+      if (control.willValidate && control.hasAttribute('aria-invalid')) {
+        control.setAttribute('aria-invalid', String(!control.validity.valid))
+      }
     })
   }
 })()