配了三年自定义组件,每次想给内部状态加个样式都要写一堆 JS——今天发现浏览器自己会读了,:state() 把这件事彻底原生化了
以前写 Web Component,组件内部状态一变,外部想单独给它加样式?对不起,CSS 读不到。
这问题折腾了前端圈很久。组件状态是 JS 管的,样式是 CSS 管的,两边井水不犯河水——你要么在组件里暴露属性,要么在外部用复杂的属性观察器,反正没有原生方式让 CSS 直接「问」组件:「你现在在哪个状态?」
2024 年 5 月,:state() 伪类和 CustomStateSet 进了 Baseline 2024,这堵墙终于被拆了。
它怎么工作的
组件内部用 attachInternals() 把状态注册进 CustomStateSet,外部直接用 CSS 伪类读:
// 组件内部
class LabeledCheckbox extends HTMLElement {
constructor() {
super();
this._internals = this.attachInternals();
}
set checked(val) {
val ? this._internals.states.add(checked)
: this._internals.states.delete(checked);
}
}
/* 外部直接读 */
labeled-checkbox:state(checked) {
border: solid;
}
这就是全部。不需要属性观察,不需要自定义事件,外部 CSS 直接拿到组件的内部状态。
三个关键细节
第一,只能给自主定制元素(autonomous custom element)用,继承自原生 HTML 元素的定制元素不行,比如基于 <button> 的自定义按钮就做不到。
第二,状态不暴露给外部的 DOM——它是组件私有的,CustomStateSet 存在 ElementInternals 里面,外部读不到具体值,只有 CSS 能通过 :state() 匹配到。
第三,状态可以在组件内部用 :host(:state(xxx)) 读到,也可以在 shadow parts 上用 ::part(xxx):state(xxx) 读到。这两层隔离让组件内部的样式和外部的样式完全解耦。
实际用的时候什么样
最典型的场景是表单组件。开关、选择器、日期选择器这类有多种互斥状态的组件,以前要靠 aria-checked、disabled 这些公开属性来做样式区分,现在直接在组件内部维护状态集,外部用 :state(disabled)、:state(loading)、:state(error) 精确匹配。
代码示例:
class DatePicker extends HTMLElement {
set loading(val) {
val ? this._internals.states.add(loading)
: this._internals.states.delete(loading);
}
set error(msg) {
this._internals.states.add(error);
this._internals.states.add(msg); // 错误信息也可作为状态
}
}
date-picker:state(loading) { opacity: 0.6; pointer-events: none; }
date-picker:state(error) { border-color: red; }
现在能不能用
Baseline 2024 = Chrome 126+、Firefox 130+、Safari 17.5+ 全部支持。生产环境直接用,不需要任何 polyfill。
下一步
如果你现在在维护一套设计系统组件库,把内部状态管理从「暴露公开属性 + 外部监听」切换到 CustomStateSet + :state(),可以让组件的样式 API 干净很多——外部只需要知道有哪些状态可以样式覆盖,不需要知道这些状态对应什么属性或事件。
已经有组件库在这么做了,比如 Shoelace 下一步规划里就把这个列进了计划。如果你用的是其他组件库,可以看看它们有没有跟进这个能力。
评论区
登录后可评论。