自定义组件状态 CSS 读不到我折腾了三年——今天 :state() 终于把这件事说清楚了
以前给自定义组件写样式,外界只知道它「存在」,不知道它内部在干什么——现在 CSS 终于能直接读到组件自己暴露的状态了。
这件事的转折点是 :state() 伪类和 CustomStateSet。简单说,自定义组件内部可以往自己的 CustomStateSet 里添加或删除状态字符串,CSS 选择器就能用 :state(状态名) 直接匹配到它。
先看一个最基础的例子——一个抽屉组件,对外暴露 open 和 closed 两个状态:
class DrawerPanel extends HTMLElement {
constructor() {
super();
this._states = new CustomStateSet(this);
}
open() {
this._states.add("open");
this._states.delete("closed");
}
close() {
this._states.add("closed");
this._states.delete("open");
}
set loading(val) {
if (val) this._states.add("loading");
else this._states.delete("loading");
}
}
customElements.define("drawer-panel", DrawerPanel);
CSS 这边直接就能用了,不需要任何 class 绑定:
/* 匹配 open 状态 */
drawer-panel:state(open) {
transform: translateX(0);
box-shadow: 0 0 20px rgba(0,0,0,0.2);
}
/* 匹配 loading 状态 */
drawer-panel:state(loading) {
opacity: 0.7;
pointer-events: none;
}
/* 组合多个状态 */
drawer-panel:state(open):state(loading) {
/* 打开中的加载状态 */
}
这就是关键区别——以前你得自己在组件上 toggle class,JS 和 CSS 各管各的,现在 CSS 直接订阅组件内部状态,JS 只需要负责往 CustomStateSet 里写数据。
再来看一个实际场景:表单组件对外暴露 valid、invalid、dirty 三个状态。
class SmartInput extends HTMLElement {
constructor() {
super();
this._states = new CustomStateSet(this);
}
connectedCallback() {
this._input = this.querySelector("input");
this._input.addEventListener("blur", () => this._validate());
this._input.addEventListener("input", () => {
this._states.add("dirty");
this._validate();
});
}
_validate() {
if (this._input.validity.valid) {
this._states.add("valid");
this._states.delete("invalid");
} else {
this._states.add("invalid");
this._states.delete("valid");
}
}
}
CSS 写法:
smart-input:state(invalid) {
border-color: red;
}
smart-input:state(invalid):state(focused) {
border-color: darkred;
box-shadow: 0 0 0 2px rgba(255,0,0,0.2);
}
smart-input:state(valid) {
border-color: green;
}
smart-input:state(dirty):state(invalid) {
/* 用户改过了且当前无效 */
animation: shake 0.3s ease-in-out;
}
注意 :state() 接收的是一个自定义标识符,不是 :has() 那种复杂选择器。它适合的场景是:组件自己定义一组有限状态,CSS 按状态渲染样式。
如果你的组件是 popover 或 details 这种浏览器内置组件,也有内置状态可以用:
/* 浏览器内置状态 */
popover:state(open) {
opacity: 1;
transform: scale(1);
}
浏览器兼容性方面:Chrome 129+ 和 Safari 18+ 已经支持,Firefox 还没上。实际项目里可以照常写,配合 @supports 做渐进增强:
drawer-panel {
/* 默认降级样式 */
display: none;
}
@supports selector(drawer-panel:state(open)) {
drawer-panel:state(open) {
display: block;
}
drawer-panel:not(:state(open)) {
display: none;
}
}
最后说一个很多人会踩的坑::state() 里的状态名是大小写敏感的,state(“Open”) 和 state(“open”) 是两个完全不同的状态。所以组件内部定义状态常量的时候,统一用小写加连字符的写法,比如 state(“loading-skeleton”) 而不是 state(“LoadingSkeleton”)。
落地步骤:
- 盘点项目里有哪些自定义组件目前靠 class toggle 来控制样式
- 把这些组件的内部状态抽象成有限集合(比如 open/closed、loading/idle、selected/unselected)
- 用 CustomStateSet 替换手动 class 操作,CSS 用 :state() 直接匹配
- 加上 @supports 降级,确保 Firefox 用户体验不降级
评论区
登录后可评论。