必须手动控制 disabled 属性、替换按钮内容,并在 finally 块中统一恢复——Bootstrap 仅提供 spinner-border 样式,不处理禁用逻辑、无障碍支持及错误后状态还原。

必须手动控制 disabled 属性、替换按钮内容,并在 finally 块中统一恢复——Bootstrap 本身不提供自动切换逻辑,只提供 spinner-border 样式和基础类。
点击后按钮仍可重复提交?检查是否漏设 disabled
常见错误是只改文字或加 spinner,但没真正禁用按钮。浏览器原生 disabled 是唯一能拦截鼠标点击、键盘回车、屏幕阅读器操作的手段。
- 用
btn.disabled = true(不是setAttribute('disabled', 'true')),确保表单内回车也失效 - 别信“加
opacity: 0.6就够了”——没设pointer-events: none的按钮在移动端仍可能穿透点击 - 必须同时设置
aria-disabled="true"和aria-busy="true",否则读屏器无法感知状态变化 - 用
<button type="button">,而非<button type="submit">;后者在表单内会触发两次:一次 JS 事件、一次隐式 submit
加载中文字 + spinner 怎么拼才不跳动?
直接 innerHTML = '加载中' 或 textContent = '加载中' 会导致按钮高度突变、文字上移。原因是 spinner-border 默认是行内元素,会参与流式布局并撑高容器。
- 给按钮加
position: relative和overflow: hidden - 把
spinner-border用position: absolute居中:top: 50%; left: 50%; transform: translate(-50%, -50%) - 原始文字包在
<span class="btn-text">提交</span>里,加载时设display: none,避免堆叠 - 尺寸别写死,用
1.2em(对应btn-sm)或1.5em(对应btn-md),保持与文本字号一致 - 务必加
role="status"和aria-hidden="true",防止读屏器重复读“加载中”
请求失败后按钮卡死?finally 不是可选项
90% 的 loading 按钮 bug 出现在错误分支:网络超时、400/500 响应后,按钮永远停在 disabled + spinner 状态,用户无法重试。
- 恢复逻辑必须写在
fetch().finally()或try/catch/finally里,不能只放在.then() - 还原时用
btn.removeAttribute('disabled'),不是btn.disabled = false(后者在某些旧版 Safari 下不触发样式重绘) - 原始 HTML 要提前存进
dataset:btn.dataset.originalHtml = btn.innerHTML,还原时直接赋值,保留图标、data-*属性等结构 - 移除 spinner 元素时用
spinnerEl.remove(),别靠innerHTML覆盖——容易删掉其他动态插入的内容
要不要用 Bootstrap 自带的 data-loading-text?
那是 Bootstrap 3 的老方案,依赖 jQuery 和 bootstrap-button.js,现代项目(尤其是 Bootstrap 5+)已废弃。它不支持 spinner、不处理 aria、无法应对 Promise 失败场景,且与模块化构建工具冲突。
- 别再写
<button data-loading-text="加载中">+$(this).button('loading') - 当前标准做法是纯原生 JS 控制:存原始内容 → 禁用 → 插入绝对定位 spinner → finally 还原
- 如果用 Blazor,优先走
IsDisabled+ 条件渲染<Spinner>,框架已内置状态同步和无障碍支持
最易被忽略的点:按钮原始 HTML 中的空格、换行、图标标签(如 <i class="bi bi-check"></i>)必须完整保留,否则还原后样式错位或图标消失。建议用 dataset.originalHtml 存,不用 textContent。


















