button:disabled 和 button[disabled] 必须同时写,前者匹配运行时禁用状态,后者匹配 dom 属性存在,二者覆盖 ssr、框架动态控制等不同链路,缺一不可。

button:disabled 和 button[disabled] 必须同时写
只写 button:disabled 在本地开发常能蒙混过关,但上线后 SSR 渲染的按钮、React 中用 disabled={true} 动态控制的按钮、甚至手动调用 el.setAttribute('disabled', '') 的场景,都会漏掉样式。根本原因是:button:disabled 匹配的是浏览器“运行时识别为禁用”的状态(JS 赋值 el.disabled = true 即可触发),而 button[disabled] 匹配的是 DOM 上真实存在的 disabled 属性(SSR 输出、setAttribute、框架未透传属性时全靠它)。二者覆盖不同链路,缺一不可。
扩展到其他控件也一样:input:disabled, input[disabled], select:disabled, select[disabled]。自定义组件如 <mybutton disabled></mybutton>,若底层没透传到原生 button,就得额外加类名支持:.my-button:disabled, .my-button[disabled]。
禁用样式不能只靠 opacity 或 filter
单独写 opacity: 0.5 或 filter: grayscale(1) 是高危操作:前者让文字对比度在深色模式下极易跌破 WCAG 4.5:1;后者在 iOS Safari 旧版可能渲染成全黑,且两者都不改光标、不阻断键盘焦点、不通知屏幕阅读器——用户仍会尝试点击或用 Tab 键聚焦。
必须显式控制多个通道:
-
color: #666(别用#999,保对比度) -
background-color: #e0e0e0(比#cccccc更稳妥) border-color: #ddd-
cursor: not-allowed(:disabled不自动加) -
transition: none(防:hover残留)
如果按钮有内联 style="cursor: pointer",button:disabled 规则会被覆盖,得加 !important 或提升选择器权重(如 .btn:disabled)。
非原生按钮(div / 自定义组件)根本不匹配 :disabled
:disabled 只对 <button></button>、<input>、<select></select>、<textarea></textarea> 这四类原生可禁用元素生效。给 <div role="button"> 或封装组件加 <code>disabled 属性,CSS 完全无视,JS 事件也不自动拦截。
正确做法只有两条路:
- 优先换回语义化
<button type="button"></button>,别模拟按钮 - 必须用自定义组件时,用 class 控制状态:
<my-button class="is-disabled"></my-button>,再配.is-disabled { cursor: not-allowed; color: #666; },并同步设置aria-disabled="true"和tabindex="-1"
JS 事件处理开头必须加 if (el.disabled || el.hasAttribute('aria-disabled')) return;,否则视觉禁用 ≠ 行为禁用。
:enabled 不是 :disabled 的反向兜底
别写 button:enabled { ... } 来“补启用态”,它不是 :disabled 的反向逻辑。真正需要的是明确排除禁用状态:button:not(:disabled)。因为 :enabled 会匹配所有未被禁用的按钮,包括那些压根没设 disabled 属性、但本该默认启用的按钮——这会导致样式污染和维护混乱。
更关键的是,:enabled 同样不匹配非原生元素,且无法覆盖 SSR 或框架未透传属性导致的“假启用”问题。所以统一用 :not(:disabled) 更可靠,也更符合实际渲染逻辑。
最易被忽略的一点:禁用态样式规则权重太低,常被 .btn-primary:hover(权重 20)盖过 button[disabled](权重 10),检查 DevTools 里是否被划掉;还有,button:disabled:hover 这种组合必须显式重置,否则悬停态颜色会偷偷继承过来。











