跳到主要内容
输入关键词开始搜索,↑ ↓ 选择,Enter 打开
没有找到匹配的文档

表单校验

通过浏览器默认行为或自定义样式和 JavaScript,使用 HTML5 表单验证向你的用户提供有价值的、可操作的反馈。

层级:components

它是如何工作的

以下是 Bootstrap 的表单验证工作原理:

  • 客户端验证 使用 CSS 伪类并作用于后面的范围 data-bs-validate 属性。添加 data-bs-validate 到你的 <form> 选择加入 :user-invalid 样式——仅显示验证样式 在用户交互之后 通过一个控件(例如,输入和失焦,或尝试提交),以便在页面加载时必填字段不会显示为无效。
  • 默认情况下, 客户端验证只显示无效的样式。 要同时显示有效字段的成功样式,请设置 data-bs-validate="valid".
  • 服务器端验证 使用 .is-invalid 和 .is-valid 类用于指示字段状态,而无需用户互动。这些在全局有效——无需 data-bs-validate 需要属性。
  • 自定义验证状态之外 valid/invalid (e.g., warning) 通过 .is-* 类模式和 $validation-states Sass map。自定义状态没有生成的伪类。
  • 所有现代浏览器都支持 约束验证 API,一系列用于验证表单控件的 JavaScript 方法。
  • 反馈信息可能会使用 浏览器默认设置 (每个浏览器不同,并且无法通过 CSS 样式化)或使用我们自定义的反馈样式,附加 HTML 和 CSS。
  • 你可以提供自定义有效性消息 setCustomValidity 在 JavaScript 中。

考虑到这一点,请查看以下我们自定义表单验证样式、可选服务器端类以及浏览器默认设置的演示。

自定义样式

对于自定义 Bootstrap 表单验证消息,请将 data-bs-validate 和 novalidate 布尔属性添加到你的 <form> 上。data-bs-validate 属性允许使用 Bootstrap 的 :user-invalid 样式,而 novalidate 会禁用浏览器默认的反馈工具提示。自定义反馈样式会应用自定义颜色、边框和焦点样式,以更好地传达反馈信息。

使用 data-bs-validate="valid" 也可以在有效字段上显示成功样式。向每个表单输入添加 required,并使用 .invalid-feedback 提供字段特定的错误消息。如果启用有效样式,请使用 .valid-feedback 显示成功消息。

为了无障碍访问,在每个表单控件上添加 aria-describedby,指向其反馈消息的 id。这确保了当用户聚焦于无效字段时,屏幕阅读器会提示错误。下面的示例 JavaScript 还会在提交时为每个控件设置 aria-invalid,并在用户纠正输入时清除它。

例如,尝试提交下面的表单;我们的 JavaScript 会拦截提交按钮并向你传递反馈。

[!代码 高亮]
[!代码 高亮]
需要全名
[!代码 高亮]
$ [!代码 高亮]
请输入捐款金额
[!代码 高亮]
[!代码 高亮]
请输入邮寄地址
[!代码 高亮]
[!代码 高亮]
请提供一个城市
[!代码 高亮]
请选择一个州
[!代码 高亮]
[!代码 高亮]
必填
[!代码 高亮]
[!代码 高亮]
预计5-7个工作日送达。
[!代码 高亮]
2-3个工作日内到达。
HTML
<form class="grid grid-cols-1 gap-5" data-bs-validate novalidate>
  <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 aria-describedby="validationCustom01Feedback">
      <div id="validationCustom01Feedback" class="invalid-feedback">Full name is required</div>
    </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 validationCustomUsernameFeedback" required>
      </div>
      <div id="validationCustomUsernameFeedback" class="invalid-feedback">Please enter a donation amount</div>
    </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 aria-describedby="validationCustomAddressFeedback">
    <div id="validationCustomAddressFeedback" class="invalid-feedback">Please enter a mailing address</div>
  </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 aria-describedby="validationCustom03Feedback">
      <div id="validationCustom03Feedback" class="invalid-feedback">Please provide a city</div>
    </div>
    <div class="form-field md:g-col-4">
      <label for="validationCustom04" class="form-label">State</label>
      <select class="form-control" id="validationCustom04" required aria-describedby="validationCustom04Feedback">
        <option selected disabled value="">Choose…</option>
        <option>...</option>
      </select>
      <div id="validationCustom04Feedback" class="invalid-feedback">Please select a state</div>
    </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 aria-describedby="validationCustom05Feedback">
      <div id="validationCustom05Feedback" class="invalid-feedback">Required</div>
    </div>
  </div>

  <div class="form-group">
    <label class="form-label">Shipping method</label>
    <div class="form-field mb-1">
      <input type="radio" id="validationCustomRadio1" class="radio" name="shipping" required>
      <div class="form-field-content">
        <label for="validationCustomRadio1">Standard</label>
        <small class="form-text">Arrives in 5-7 business days.</small>
      </div>
    </div>
    <div class="form-field">
      <input type="radio" id="validationCustomRadio2" class="radio" name="shipping" required>
      <div class="form-field-content">
        <label for="validationCustomRadio2">Express</label>
        <small class="form-text">Arrives in 2-3 business days.</small>
      </div>
    </div>
  </div>

  <div class="form-field">
    <div class="switch">
      <input type="checkbox" id="validationCustomSwitch" role="switch" switch>
    </div>
    <label for="validationCustomSwitch">Subscribe to newsletter</label>
  </div>

  <button class="btn-solid theme-primary justify-self-start" type="submit">Submit form</button>
</form>
JavaScript
// Example starter JavaScript for preventing form submissions when there are invalid fields
(() => {
  'use strict'

  const forms = document.querySelectorAll('form[data-bs-validate]')

  for (const form of forms) {
    form.addEventListener('submit', event => {
      if (!form.checkValidity()) {
        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))
      }
    })
  }
})()

浏览器默认设置

不想要自定义验证反馈消息或编写 JavaScript 来更改表单行为?没问题,你可以使用浏览器默认设置。试着提交下面的表单。根据你的浏览器和操作系统,你会看到略微不同风格的反馈。

虽然这些反馈样式不能通过 CSS 进行样式化,但你仍然可以通过 JavaScript 自定义反馈文本。

$
预计5-7个工作日到达。
2-3个工作日内到达。
HTML
<form class="grid grid-cols-1 gap-5">
  <div class="grid gap-5">
    <div class="form-field md:g-col-6">
      <label for="validationDefault01" class="form-label">Name</label>
      <input type="text" class="form-control" id="validationDefault01" value="Mark Otto" required>
    </div>
    <div class="form-field md:g-col-6">
      <label for="validationDefaultDonation" class="form-label">Donation amount</label>
      <div class="input-group">
        <span class="input-group-text" id="inputGroupPrepend2">$</span>
        <input type="number" class="form-control" value="100" id="validationDefaultDonation" aria-describedby="inputGroupPrepend2" required>
      </div>
    </div>
  </div>

  <div class="form-field">
    <label for="validationDefaultAddress" class="form-label">Address</label>
    <input type="text" class="form-control" id="validationDefaultAddress" value="1234 Main St" required>
  </div>

  <div class="grid gap-5">
    <div class="form-field md:g-col-6">
      <label for="validationDefault03" class="form-label">City</label>
      <input type="text" class="form-control" id="validationDefault03" required>
    </div>
    <div class="form-field md:g-col-4">
      <label for="validationDefault04" class="form-label">State</label>
      <select class="form-control" id="validationDefault04" required>
        <option selected disabled value="">Choose…</option>
        <option>...</option>
      </select>
    </div>
    <div class="form-field md:g-col-2">
      <label for="validationDefault05" class="form-label">Zip</label>
      <input type="text" class="form-control" id="validationDefault05" required>
    </div>
  </div>

  <div class="form-group">
    <label class="form-label">Shipping method</label>
    <div class="form-field mb-1">
      <input type="radio" id="validationDefaultRadio1" class="radio" name="shippingDefault" required>
      <div class="form-field-content">
        <label for="validationDefaultRadio1">Standard</label>
        <small class="form-text">Arrives in 5-7 business days.</small>
      </div>
    </div>
    <div class="form-field">
      <input type="radio" id="validationDefaultRadio2" class="radio" name="shippingDefault" required>
      <div class="form-field-content">
        <label for="validationDefaultRadio2">Express</label>
        <small class="form-text">Arrives in 2-3 business days.</small>
      </div>
    </div>
  </div>

  <div class="form-field">
    <div class="switch">
      <input type="checkbox" id="validationDefaultSwitch" role="switch" switch>
    </div>
    <label for="validationDefaultSwitch">Subscribe to newsletter</label>
  </div>

  <button class="btn-solid theme-primary justify-self-start" type="submit">Submit form</button>
</form>

服务器端

我们建议使用客户端验证,但如果你需要服务器端验证,你可以使用.is-invalid和.is-valid来标记无效和有效的表单字段。请注意,这些类也支持.invalid-feedback。

对于无效的字段,确保将无效反馈/错误信息与相关表单字段关联,使用 aria-describedby(请注意,该属性允许引用多个 id,以防该字段已经指向其他表单文本)。

[!代码 高亮]
看起来很棒!
[!代码 高亮]
需要全名
[!代码 高亮]
$ [!代码 高亮]
看起来很棒!
[!代码 高亮]
请输入捐款金额
[!代码 高亮]
[!代码 高亮]
看起来很棒!
[!代码 高亮]
请输入邮寄地址
[!代码 高亮]
请提供一个城市
请选择一个州
[!代码 高亮]
必填
[!代码 高亮]
预计5-7个工作日送达。
[!代码 高亮]
2-3个工作日内到达。
HTML
<form class="grid grid-cols-1 gap-5">
  <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" aria-describedby="validationServer01Valid validationServer01Invalid">
      <div id="validationServer01Valid" class="valid-feedback">Looks great!</div>
      <div id="validationServer01Invalid" class="invalid-feedback">Full name is required</div>
    </div>
    <div class="form-field md:g-col-6">
      <label for="validationServerDonation" class="form-label">Donation amount</label>
      <div class="input-group">
        <span class="input-group-text" id="inputGroupPrepend3">$</span>
        <input type="number" class="form-control is-invalid" value="" id="validationServerDonation" aria-describedby="inputGroupPrepend3 serverDonateFeedbackValid serverDonateFeedbackInvalid">
      </div>
      <div id="serverDonateFeedbackValid" class="valid-feedback">Looks great!</div>
      <div id="serverDonateFeedbackInvalid" class="invalid-feedback">Please enter a donation amount</div>
    </div>
  </div>

  <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" aria-describedby="validationServerAddressValid validationServerAddressInvalid">
    <div id="validationServerAddressValid" class="valid-feedback">Looks great!</div>
    <div id="validationServerAddressInvalid" class="invalid-feedback">Please enter a mailing address</div>
  </div>

  <div class="grid gap-5">
    <div class="form-field md:g-col-6">
      <label for="validationServer03" class="form-label">City</label>
      <input type="text" class="form-control is-invalid" id="validationServer03" aria-describedby="validationServer03Feedback">
      <div id="validationServer03Feedback" class="invalid-feedback">Please provide a city</div>
    </div>
    <div class="form-field md:g-col-4">
      <label for="validationServer04" class="form-label">State</label>
      <select class="form-control is-invalid" id="validationServer04" aria-describedby="validationServer04Feedback">
        <option selected disabled value="">Choose…</option>
        <option>...</option>
      </select>
      <div id="validationServer04Feedback" class="invalid-feedback">Please select a state</div>
    </div>
    <div class="form-field md:g-col-2">
      <label for="validationServer05" class="form-label">Zip</label>
      <input type="text" class="form-control is-invalid" id="validationServer05" aria-describedby="validationServer05Feedback">
      <div id="validationServer05Feedback" class="invalid-feedback">Required</div>
    </div>
  </div>

  <div class="form-group">
    <label class="form-label">Shipping method</label>
    <div class="form-field mb-1">
      <input type="radio" id="validationServerRadio1" class="radio is-invalid" name="shippingServer">
      <div class="form-field-content">
        <label for="validationServerRadio1">Standard</label>
        <small class="form-text">Arrives in 5-7 business days.</small>
      </div>
    </div>
    <div class="form-field">
      <input type="radio" id="validationServerRadio2" class="radio is-invalid" name="shippingServer">
      <div class="form-field-content">
        <label for="validationServerRadio2">Express</label>
        <small class="form-text">Arrives in 2-3 business days.</small>
      </div>
    </div>
  </div>

  <div class="form-field">
    <div class="switch">
      <input type="checkbox" id="validationServerSwitch" role="switch" switch>
    </div>
    <label for="validationServerSwitch">Subscribe to newsletter</label>
  </div>

  <button class="btn-solid theme-primary justify-self-start" type="submit">Submit form</button>
</form>

支持的元素

以下表单控件和组件可用验证样式:

  • <input>和 <textarea>带有s .form-control
  • <select>带有s .form-control
  • .check 复选框
  • .radio 收音机
  • .switch 开关
  • .form-range-input 范围输入
  • .form-floating 浮动标签
  • .form-adorn 装饰输入
  • .chip-input 芯片输入
  • .combobox-toggle 组合框(仅限基于类的)
  • 输入组(带有括号内的反馈 .form-field)
请在文本区域输入消息。
示例无效反馈文本
更多示例无效反馈文本
示例无效开关反馈
示例无效选择反馈
示例无效表单文件反馈
示例无效范围反馈
示例无效浮动标签反馈
示例无效的装饰输入反馈
@
请选择一个用户名
例子
请至少添加一个标签
HTML
<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 aria-describedby="validationTextareaFeedback"></textarea>
    <div id="validationTextareaFeedback" class="invalid-feedback">
      Please enter a message in the textarea.
    </div>
  </div>

  <div class="form-group">
    <label class="form-label">Checkbox</label>
    <div class="form-field">
      <input type="checkbox" class="check is-invalid" id="validationFormCheck1" required aria-describedby="validationFormCheck1Feedback">
      <div class="form-field-content">
        <label for="validationFormCheck1">Check this checkbox</label>
        <div id="validationFormCheck1Feedback" class="invalid-feedback">Example invalid feedback text</div>
      </div>
    </div>
  </div>

  <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 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 aria-describedby="validationFormRadioFeedback">
      <div class="form-field-content">
        <label for="validationFormCheck3">Or toggle this other radio</label>
        <div id="validationFormRadioFeedback" class="invalid-feedback">More example invalid feedback text</div>
      </div>
    </div>
  </div>

  <div class="form-group">
    <label class="form-label">Switch</label>
    <div class="form-field">
      <div class="switch">
        <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 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 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 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 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>
    <div class="form-range">
      <input type="range" class="form-range-input is-invalid" id="validationRange" min="0" max="5" required aria-describedby="validationRangeFeedback">
    </div>
    <div id="validationRangeFeedback" class="invalid-feedback">Example invalid range feedback</div>
  </div>

  <div class="form-field">
    <div class="form-floating">
      <label for="validationFloating">Email address</label>
      <input type="email" class="form-control is-invalid" id="validationFloating" placeholder="name@example.com" value="test@example.com" aria-describedby="validationFloatingFeedback">
    </div>
    <div id="validationFloatingFeedback" class="invalid-feedback d-block">Example invalid floating label feedback</div>
  </div>

  <div class="form-field">
    <label for="validationAdorn" class="form-label">Adorned input</label>
    <label class="form-control form-adorn is-invalid">
      <div class="form-adorn-icon" aria-hidden="true">
        <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..." aria-describedby="validationAdornFeedback">
    </label>
    <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" aria-describedby="validationInputGroupFeedback">
    </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..." aria-describedby="validationChipInputFeedback">
    </div>
    <div id="validationChipInputFeedback" class="invalid-feedback">Please add at least one tag</div>
  </div>
</form>

工具提示

如果你的表单布局允许,你可以将 .{valid|invalid}-feedback 类替换为 .{valid|invalid}-tooltip 类,以在样式化的工具提示中显示验证反馈。这些工具提示基于基本的 .tooltip 样式构建,因此你也需要在编译后的 CSS 中包含这些样式。

我们不建议在复选框、单选按钮和开关上使用工具提示,因为在大多数情况下,它们的位置不可靠且不一致。你也可以在表单中结合使用反馈文本和工具提示。

此示例的功能与自定义样式示例相同。点击提交按钮以查看验证提示。

[!代码 高亮]
需要全名
[!代码 高亮]
$
请输入捐款金额
[!代码 高亮]
请输入邮寄地址
[!代码 高亮]
请提供一个城市
[!代码 高亮]
请选择一个州
[!代码 高亮]
必填
[!代码 高亮]
预计5-7个工作日到达。
请选择一种运输方式
[!代码 高亮]
2-3个工作日内到达。
HTML
<form class="grid grid-cols-1 gap-5" data-bs-validate novalidate>
  <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 aria-describedby="validationTooltip01Feedback">
      <div id="validationTooltip01Feedback" class="tooltip invalid-tooltip">Full name is required</div>
    </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 validationTooltipDonationFeedback" required>
      </div>
      <div id="validationTooltipDonationFeedback" class="tooltip invalid-tooltip">Please enter a donation amount</div>
    </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 aria-describedby="validationTooltipAddressFeedback">
    <div id="validationTooltipAddressFeedback" class="tooltip invalid-tooltip">Please enter a mailing address</div>
  </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 aria-describedby="validationTooltip03Feedback">
      <div id="validationTooltip03Feedback" class="tooltip invalid-tooltip">Please provide a city</div>
    </div>
    <div class="form-field md:g-col-4">
      <label for="validationTooltip04" class="form-label">State</label>
      <select class="form-control" id="validationTooltip04" required aria-describedby="validationTooltip04Feedback">
        <option selected disabled value="">Choose…</option>
        <option>...</option>
      </select>
      <div id="validationTooltip04Feedback" class="tooltip invalid-tooltip">Please select a state</div>
    </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 aria-describedby="validationTooltip05Feedback">
      <div id="validationTooltip05Feedback" class="tooltip invalid-tooltip">Required</div>
    </div>
  </div>

  <div class="form-group">
    <label class="form-label">Shipping method</label>
    <div class="form-field mb-1">
      <input type="radio" id="validationTooltipRadio1" class="radio" name="shippingTooltip" required>
      <div class="form-field-content">
        <label for="validationTooltipRadio1">Standard</label>
        <small class="form-text">Arrives in 5-7 business days.</small>
        <div class="invalid-feedback">Please select a shipping method</div>
      </div>
    </div>
    <div class="form-field">
      <input type="radio" id="validationTooltipRadio2" class="radio" name="shippingTooltip" required>
      <div class="form-field-content">
        <label for="validationTooltipRadio2">Express</label>
        <small class="form-text">Arrives in 2-3 business days.</small>
      </div>
    </div>
  </div>

  <div class="form-field">
    <div class="switch">
      <input type="checkbox" id="validationTooltipSwitch" role="switch" switch>
    </div>
    <label for="validationTooltipSwitch">Subscribe to newsletter</label>
  </div>

  <button class="btn-solid theme-primary justify-self-start" type="submit">Submit form</button>
</form>

CSS

Sass 混入

我们使用一个大型混入,form-validation-state,从状态名称和主题颜色生成所有验证反馈样式。通过Sass 循环调用此混入来编译 CSS。

@mixin form-validation-state($state, $theme) {
  .#{$state}-feedback {
    display: none;
    width: 100%;
    font-size: var(--font-size-sm);
    color: var(--#{$theme}-fg);
  }

  // More specific to override base tooltip styles
  .tooltip.#{$state}-tooltip {
    position: absolute;
    top: 100%;
    z-index: 5;
    display: none;
    max-width: 100%;
    padding: var(--tooltip-padding-y) var(--tooltip-padding-x);
    margin-top: .1rem;
    color: var(--#{$theme}-contrast);
    text-align: center;
    background-color: var(--#{$theme}-bg);
    opacity: 1;
    @include border-radius(var(--tooltip-border-radius));
  }

  // Generic sibling feedback display — works for .form-control, .form-range,
  // and any element where feedback is a direct sibling.
  @include form-validation-state-selector($state) {
    ~ .#{$state}-feedback,
    ~ .#{$state}-tooltip {
      display: block;
    }
  }

  // Form control
  .form-control {
    @include form-validation-state-selector($state) {
      --control-border-color: var(--#{$theme}-border);

      &:focus-visible {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        --control-border-color: var(--#{$theme}-border);
      }
    }
  }

  // Checkbox — control-level styling (border, checked bg, focus ring).
  .check {
    @include form-validation-state-selector($state) {
      --check-border-color: var(--#{$theme}-border);
      --check-checked-bg: var(--#{$theme}-bg);
      --check-checked-border-color: var(--#{$theme}-bg);

      &:focus-visible {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  }

  // Checkbox — label color and feedback display via .form-field:has().
  .form-field:has(.check.is-#{$state}) {
    label { color: var(--#{$theme}-fg); }

    .#{$state}-feedback,
    .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-field:has(.check:user-invalid) {
      label { color: var(--#{$theme}-fg); }

      .invalid-feedback,
      .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-field:has(.check:user-valid) {
      label { color: var(--#{$theme}-fg); }

      .valid-feedback,
      .valid-tooltip { display: block; }
    }
  }

  // Radio — control-level styling.
  .radio {
    @include form-validation-state-selector($state) {
      --radio-border-color: var(--#{$theme}-border);
      --radio-checked-bg: var(--#{$theme}-bg);
      --radio-checked-border-color: var(--#{$theme}-bg);

      &:focus-visible {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  }

  // Radio — label color and feedback display via .form-field:has().
  .form-field:has(.radio.is-#{$state}) {
    label { color: var(--#{$theme}-fg); }

    .#{$state}-feedback,
    .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-field:has(.radio:user-invalid) {
      label { color: var(--#{$theme}-fg); }

      .invalid-feedback,
      .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-field:has(.radio:user-valid) {
      label { color: var(--#{$theme}-fg); }

      .valid-feedback,
      .valid-tooltip { display: block; }
    }
  }

  // Switch — control-level styling. The input is an invisible overlay;
  // all visuals are on the .switch wrapper.
  .switch:has(input.is-#{$state}) {
    --switch-border-color: var(--#{$theme}-border);
    --switch-checked-bg: var(--#{$theme}-bg);
    --switch-checked-border-color: var(--#{$theme}-bg);

    &:focus-within {
      @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
    }
  }

  @if $state == "invalid" {
    [data-bs-validate] .switch:has(input:user-invalid) {
      --switch-border-color: var(--#{$theme}-border);
      --switch-checked-bg: var(--#{$theme}-bg);
      --switch-checked-border-color: var(--#{$theme}-bg);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .switch:has(input:user-valid) {
      --switch-border-color: var(--#{$theme}-border);
      --switch-checked-bg: var(--#{$theme}-bg);
      --switch-checked-border-color: var(--#{$theme}-bg);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  }

  // Switch — label color and feedback display via .form-field:has().
  .form-field:has(.switch input.is-#{$state}) {
    label { color: var(--#{$theme}-fg); }

    .#{$state}-feedback,
    .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-field:has(.switch input:user-invalid) {
      label { color: var(--#{$theme}-fg); }

      .invalid-feedback,
      .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-field:has(.switch input:user-valid) {
      label { color: var(--#{$theme}-fg); }

      .valid-feedback,
      .valid-tooltip { display: block; }
    }
  }

  // Chip input — wrapper has the visible border; the .form-ghost inside
  // receives the native pseudo-class.
  .chip-input:has(.form-ghost.is-#{$state}) {
    border-color: var(--#{$theme}-border);

    &:focus-within {
      @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      border-color: var(--#{$theme}-border);
    }

    ~ .#{$state}-feedback,
    ~ .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .chip-input:has(.form-ghost:user-invalid) {
      border-color: var(--#{$theme}-border);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        border-color: var(--#{$theme}-border);
      }

      ~ .invalid-feedback,
      ~ .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .chip-input:has(.form-ghost:user-valid) {
      border-color: var(--#{$theme}-border);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        border-color: var(--#{$theme}-border);
      }

      ~ .valid-feedback,
      ~ .valid-tooltip { display: block; }
    }
  }

  // Form adorn — :user-invalid fires on the inner .form-ghost, so we
  // propagate it to the visible wrapper with :has().
  .form-adorn:has(.form-ghost.is-#{$state}) {
    border-color: var(--#{$theme}-border);

    &:focus-within {
      @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      border-color: var(--#{$theme}-border);
    }

    ~ .#{$state}-feedback,
    ~ .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-adorn:has(.form-ghost:user-invalid) {
      border-color: var(--#{$theme}-border);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        border-color: var(--#{$theme}-border);
      }

      ~ .invalid-feedback,
      ~ .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-adorn:has(.form-ghost:user-valid) {
      border-color: var(--#{$theme}-border);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        border-color: var(--#{$theme}-border);
      }

      ~ .valid-feedback,
      ~ .valid-tooltip { display: block; }
    }
  }

  // Range — the validation class lives on .form-range-input, while feedback sits outside
  // the .form-range wrapper, so we use :has() to toggle it.
  .form-range-input {
    @include form-validation-state-selector($state) {
      &::-webkit-slider-thumb { background: var(--#{$theme}-bg); }
      &::-moz-range-thumb { background: var(--#{$theme}-bg); }

      &:focus-visible {
        &::-webkit-slider-thumb {
          @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        }
        &::-moz-range-thumb {
          @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        }
      }
    }
  }

  .form-range:has(.form-range-input.is-#{$state}) {
    ~ .#{$state}-feedback,
    ~ .#{$state}-tooltip { display: block; }
  }

  // Input group — feedback lives outside the input-group in the parent
  // .form-field, so we use :has() to toggle display.
  .form-field:has(.input-group .form-control.is-#{$state}) {
    .#{$state}-feedback,
    .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-field:has(.input-group .form-control:user-invalid) {
      .invalid-feedback,
      .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-field:has(.input-group .form-control:user-valid) {
      .valid-feedback,
      .valid-tooltip { display: block; }
    }
  }

  .input-group {
    > .form-control:not(:focus),
    > .form-floating:not(:focus-within) {
      @include form-validation-state-selector($state) {
        @if $state == "valid" {
          z-index: 3;
        } @else if $state == "invalid" {
          z-index: 4;
        }
      }
    }
  }

  // OTP — validation applies to the wrapper; the visual slots inherit the state.
  .otp {
    @include form-validation-state-selector($state) {
      .otp-slot {
        --otp-slot-border-color: var(--#{$theme}-border);
      }

      .otp-slot-active {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  }
}

form-validation-state-selector mixin 处理 .is-* 类和 :user-* 伪类的选择器生成。

@mixin form-validation-state-selector($state) {
  @if & {
    &.is-#{$state} {
      @content;
    }

    @if $state == "invalid" {
      @at-root [data-bs-validate] #{&}:user-invalid {
        @content;
      }
    } @else if $state == "valid" {
      @at-root [data-bs-validate~="valid"] #{&}:user-valid {
        @content;
      }
    }
  } @else {
    .is-#{$state} {
      @content;
    }

    @if $state == "invalid" {
      [data-bs-validate] :user-invalid {
        @content;
      }
    } @else if $state == "valid" {
      [data-bs-validate~="valid"] :user-valid {
        @content;
      }
    }
  }
}

Sass map

这是来自 forms/_validation.scss 的验证 Sass map。覆盖或扩展此映射以生成不同或额外的状态。

$validation-states: ();
// stylelint-disable-next-line scss/dollar-variable-default
$validation-states: defaults(
  (
    "valid": "success",
    "invalid": "danger",
  ),
  $validation-states
);
// scss-docs-end form-validation-states

// scss-docs-start form-validation-state-mixin
@mixin form-validation-state($state, $theme) {
  .#{$state}-feedback {
    display: none;
    width: 100%;
    font-size: var(--font-size-sm);
    color: var(--#{$theme}-fg);
  }

  // More specific to override base tooltip styles
  .tooltip.#{$state}-tooltip {
    position: absolute;
    top: 100%;
    z-index: 5;
    display: none;
    max-width: 100%;
    padding: var(--tooltip-padding-y) var(--tooltip-padding-x);
    margin-top: .1rem;
    color: var(--#{$theme}-contrast);
    text-align: center;
    background-color: var(--#{$theme}-bg);
    opacity: 1;
    @include border-radius(var(--tooltip-border-radius));
  }

  // Generic sibling feedback display — works for .form-control, .form-range,
  // and any element where feedback is a direct sibling.
  @include form-validation-state-selector($state) {
    ~ .#{$state}-feedback,
    ~ .#{$state}-tooltip {
      display: block;
    }
  }

  // Form control
  .form-control {
    @include form-validation-state-selector($state) {
      --control-border-color: var(--#{$theme}-border);

      &:focus-visible {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        --control-border-color: var(--#{$theme}-border);
      }
    }
  }

  // Checkbox — control-level styling (border, checked bg, focus ring).
  .check {
    @include form-validation-state-selector($state) {
      --check-border-color: var(--#{$theme}-border);
      --check-checked-bg: var(--#{$theme}-bg);
      --check-checked-border-color: var(--#{$theme}-bg);

      &:focus-visible {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  }

  // Checkbox — label color and feedback display via .form-field:has().
  .form-field:has(.check.is-#{$state}) {
    label { color: var(--#{$theme}-fg); }

    .#{$state}-feedback,
    .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-field:has(.check:user-invalid) {
      label { color: var(--#{$theme}-fg); }

      .invalid-feedback,
      .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-field:has(.check:user-valid) {
      label { color: var(--#{$theme}-fg); }

      .valid-feedback,
      .valid-tooltip { display: block; }
    }
  }

  // Radio — control-level styling.
  .radio {
    @include form-validation-state-selector($state) {
      --radio-border-color: var(--#{$theme}-border);
      --radio-checked-bg: var(--#{$theme}-bg);
      --radio-checked-border-color: var(--#{$theme}-bg);

      &:focus-visible {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  }

  // Radio — label color and feedback display via .form-field:has().
  .form-field:has(.radio.is-#{$state}) {
    label { color: var(--#{$theme}-fg); }

    .#{$state}-feedback,
    .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-field:has(.radio:user-invalid) {
      label { color: var(--#{$theme}-fg); }

      .invalid-feedback,
      .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-field:has(.radio:user-valid) {
      label { color: var(--#{$theme}-fg); }

      .valid-feedback,
      .valid-tooltip { display: block; }
    }
  }

  // Switch — control-level styling. The input is an invisible overlay;
  // all visuals are on the .switch wrapper.
  .switch:has(input.is-#{$state}) {
    --switch-border-color: var(--#{$theme}-border);
    --switch-checked-bg: var(--#{$theme}-bg);
    --switch-checked-border-color: var(--#{$theme}-bg);

    &:focus-within {
      @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
    }
  }

  @if $state == "invalid" {
    [data-bs-validate] .switch:has(input:user-invalid) {
      --switch-border-color: var(--#{$theme}-border);
      --switch-checked-bg: var(--#{$theme}-bg);
      --switch-checked-border-color: var(--#{$theme}-bg);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .switch:has(input:user-valid) {
      --switch-border-color: var(--#{$theme}-border);
      --switch-checked-bg: var(--#{$theme}-bg);
      --switch-checked-border-color: var(--#{$theme}-bg);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  }

  // Switch — label color and feedback display via .form-field:has().
  .form-field:has(.switch input.is-#{$state}) {
    label { color: var(--#{$theme}-fg); }

    .#{$state}-feedback,
    .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-field:has(.switch input:user-invalid) {
      label { color: var(--#{$theme}-fg); }

      .invalid-feedback,
      .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-field:has(.switch input:user-valid) {
      label { color: var(--#{$theme}-fg); }

      .valid-feedback,
      .valid-tooltip { display: block; }
    }
  }

  // Chip input — wrapper has the visible border; the .form-ghost inside
  // receives the native pseudo-class.
  .chip-input:has(.form-ghost.is-#{$state}) {
    border-color: var(--#{$theme}-border);

    &:focus-within {
      @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      border-color: var(--#{$theme}-border);
    }

    ~ .#{$state}-feedback,
    ~ .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .chip-input:has(.form-ghost:user-invalid) {
      border-color: var(--#{$theme}-border);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        border-color: var(--#{$theme}-border);
      }

      ~ .invalid-feedback,
      ~ .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .chip-input:has(.form-ghost:user-valid) {
      border-color: var(--#{$theme}-border);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        border-color: var(--#{$theme}-border);
      }

      ~ .valid-feedback,
      ~ .valid-tooltip { display: block; }
    }
  }

  // Form adorn — :user-invalid fires on the inner .form-ghost, so we
  // propagate it to the visible wrapper with :has().
  .form-adorn:has(.form-ghost.is-#{$state}) {
    border-color: var(--#{$theme}-border);

    &:focus-within {
      @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      border-color: var(--#{$theme}-border);
    }

    ~ .#{$state}-feedback,
    ~ .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-adorn:has(.form-ghost:user-invalid) {
      border-color: var(--#{$theme}-border);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        border-color: var(--#{$theme}-border);
      }

      ~ .invalid-feedback,
      ~ .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-adorn:has(.form-ghost:user-valid) {
      border-color: var(--#{$theme}-border);

      &:focus-within {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        border-color: var(--#{$theme}-border);
      }

      ~ .valid-feedback,
      ~ .valid-tooltip { display: block; }
    }
  }

  // Range — the validation class lives on .form-range-input, while feedback sits outside
  // the .form-range wrapper, so we use :has() to toggle it.
  .form-range-input {
    @include form-validation-state-selector($state) {
      &::-webkit-slider-thumb { background: var(--#{$theme}-bg); }
      &::-moz-range-thumb { background: var(--#{$theme}-bg); }

      &:focus-visible {
        &::-webkit-slider-thumb {
          @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        }
        &::-moz-range-thumb {
          @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
        }
      }
    }
  }

  .form-range:has(.form-range-input.is-#{$state}) {
    ~ .#{$state}-feedback,
    ~ .#{$state}-tooltip { display: block; }
  }

  // Input group — feedback lives outside the input-group in the parent
  // .form-field, so we use :has() to toggle display.
  .form-field:has(.input-group .form-control.is-#{$state}) {
    .#{$state}-feedback,
    .#{$state}-tooltip { display: block; }
  }

  @if $state == "invalid" {
    [data-bs-validate] .form-field:has(.input-group .form-control:user-invalid) {
      .invalid-feedback,
      .invalid-tooltip { display: block; }
    }
  } @else if $state == "valid" {
    [data-bs-validate~="valid"] .form-field:has(.input-group .form-control:user-valid) {
      .valid-feedback,
      .valid-tooltip { display: block; }
    }
  }

  .input-group {
    > .form-control:not(:focus),
    > .form-floating:not(:focus-within) {
      @include form-validation-state-selector($state) {
        @if $state == "valid" {
          z-index: 3;
        } @else if $state == "invalid" {
          z-index: 4;
        }
      }
    }
  }

  // OTP — validation applies to the wrapper; the visual slots inherit the state.
  .otp {
    @include form-validation-state-selector($state) {
      .otp-slot {
        --otp-slot-border-color: var(--#{$theme}-border);
      }

      .otp-slot-active {
        @include focus-ring(true, $color: var(--#{$theme}-focus-ring));
      }
    }
  }
}
// scss-docs-end form-validation-state-mixin

@layer components {
  // scss-docs-start form-validation-states-loop
  @each $state, $theme in $validation-states {
    @include form-validation-state($state, $theme);
  }
  

映射中的每个键都是一个验证状态名称(例如,"valid"),值是一个主题颜色名称(例如,"success")。所有样式都来自主题颜色的 CSS 自定义属性。

Sass 循环

用于迭代 $validation-states 映射值以生成我们的验证样式。对上面的 Sass map所做的任何修改都将通过此循环反映在你编译的 CSS 中。

@each $state, $theme in $validation-states {
  @include form-validation-state($state, $theme);
}

自定义

验证状态可以通过 Sass 使用 $validation-states 映射进行自定义。位于我们的 forms/_validation.scss 文件中,这个 Sass map将每个状态名称与主题颜色名称配对。所有样式都源自主题颜色的现有 CSS 自定义属性(--{theme}-fg、--{theme}-border、--{theme}-bg 等)。虽然浏览器不支持其他状态,但使用自定义样式的用户可以轻松添加更复杂的表单反馈。