必须同时使用 button:disabled 和 button[disabled] 双选择器,因为 :disabled 仅匹配浏览器识别的禁用状态,而 [disabled] 匹配 html 属性存在与否;漏一则导致 ssr、框架组件或动态设置时样式失效。

直接用 button:disabled 配合 button[disabled] 双选择器写样式,否则线上环境大概率失效。
为什么只写 button:disabled 不够用
浏览器原生的 :disabled 伪类只匹配“当前被浏览器识别为禁用状态”的元素——比如 SSR 渲染后 DOM 上已有 disabled 属性,或 JS 执行了 el.disabled = true。但很多框架(React/Vue)组件内部只是把 disabled 当作 prop 存着,并没透传到真实 <button></button> 标签上;有些 UI 库甚至用 data-disabled 模拟。这时候 :disabled 完全不触发。
而 button[disabled] 是属性选择器,只要 HTML 属性存在就生效,不管语义是否被浏览器识别。两者必须共存:
button:disabled,
button[disabled] {
opacity: 0.5;
background-color: #e0e0e0;
color: #999;
cursor: not-allowed;
border-color: #ddd;
transition: none;
}
- 漏掉
button[disabled]→ SSR 页面、自定义组件、手动setAttribute的按钮全无禁用样式 - 只写
button[disabled]→ 动态设el.disabled = true后样式可能不更新(老浏览器或某些渲染路径下)
pointer-events: none 不该用来禁用按钮功能
这个 CSS 属性只阻止鼠标和触摸事件,对键盘焦点、Enter/Space 触发、表单序列化提交完全无效。加了它,按钮依然能被 Tab 到、按空格照样执行 click、后台照样收到值。
真正需要的是语义禁用,不是视觉遮罩:
- 按钮必须用原生
<button></button>,别用<div role="button"> 或封装组件绕开 <code>disabled - JS 控制禁用状态时,必须用
btn.disabled = true,而不是只切 class 或设style.pointerEvents - 如果非得用
pointer-events: none(比如加载中遮罩层),要额外加tabindex="-1"和aria-disabled="true"补齐可访问性 - 必须设
cursor: not-allowed(浏览器不会自动加) - 背景色、边框色、文字色全部重置,别依赖继承
- 禁用过渡动画:
transition: none,否则 hover/focus 效果残留 - 避免
pointer-events: none+opacity组合——它会让键盘用户完全迷失焦点位置
禁用样式不能只靠 opacity 或 filter
单独用 opacity: 0.5 会让文字对比度跌破 WCAG 最低要求(尤其深色模式下不可读);filter: grayscale(1) 在旧版 Safari 里常渲染成全黑,且不改光标、不阻断焦点、不通知读屏器。
可靠做法是显式控制多个通道:
最易被忽略的一点:禁用状态不是纯视觉问题。DOM 层(disabled 属性)、CSS 层(:disabled 样式)、行为层(事件 handler 开头检查 if (el.disabled) return)三者必须同步,缺一不可。否则就会出现“看着灰了,点一下还是提交了”或者“读屏器说按钮可用,但按空格没反应”这种割裂体验。











