禁用按钮必须用 button.disabled = true,仅加 disabled 类无效;插入 spinner-border 需清空内容后创建元素并设 aria-hidden;状态恢复须在 finally 中成套执行;按钮类型应设为 type="button" 防重复提交。

必须用 button.disabled = true 禁用按钮,否则点击仍有效
只改文字或加图标,按钮照样能点——disabled 是唯一能真正阻断鼠标、键盘(空格/回车)、触屏事件的原生属性。Bootstrap 的灰化样式只是副作用,不是禁用逻辑本身。常见错误是只加 disabled 类,但 Bootstrap 5 不再响应 class="disabled" 来拦截交互,它只改颜色和透明度。
正确做法是直接操作 DOM 属性:
-
button.disabled = true(推荐,语义清晰、兼容性好) - 或
button.setAttribute('disabled', '') - 同时补上
button.setAttribute('aria-disabled', 'true')和button.tabIndex = -1,防安卓 WebView 点击穿透、提升读屏器播报准确率
用 spinner-border 插入 DOM,别靠伪元素或内联样式
伪元素旋转(如 ::after { animation: spin })会导致文字偏移、按钮宽度跳变;手动设 style="display: inline-block" 又容易被 Bootstrap 的 inline-flex 容器压制,尤其在小尺寸按钮里失效。
Bootstrap 5 自带的 spinner-border 更稳妥,但必须按规范插入:
- 先清空按钮内容:
button.innerHTML = '' - 创建并插入 spinner:
const spinner = document.createElement('span');,加类spinner-border spinner-border-sm,设role="status"和aria-hidden="true" - 再追加文案:
button.insertAdjacentText('beforeend', '提交中'); - 按钮需加
position-relative,spinner 加position-absolute top-50 start-50 translate-middle居中,否则会贴左上角或撑高容器
状态恢复必须写在 finally 块里,且成套还原
只在 .then() 里恢复,接口返回 400/500 就卡死;只在 .catch() 里恢复,成功后反而漏掉。必须统一收口到 .finally()。
还原动作不能只写 button.disabled = false —— 某些旧环境不触发样式重绘。应成套执行:
button.removeAttribute('disabled')button.removeAttribute('aria-disabled')button.tabIndex = 0-
button.innerHTML = originalHTML(保留原始图标、span结构,别只用textContent) - 若用了
AbortController,finally里也要调abortController.abort()
按钮类型要设为 type="button",避免表单重复提交
默认 type="submit" 在表单内会先触发 JS 事件,再触发表单提交,导致逻辑执行两次。这不是 spinner 的问题,是语义误用。
解决方式很简单:
- 显式声明
<button type="button"></button>,彻底切断自动提交行为 - 如果必须用
type="submit",就在事件处理函数开头加event.preventDefault() - 别依赖
data-remote或其他废弃特性,Bootstrap 5 已移除它们
visually-hidden 替代 display: none 隐藏原文案,既保障可访问性又不破坏布局;还有 spinner 尺寸要用 em(如 1.2em)而非固定像素,否则在缩放字体或响应式场景下错位。











