:defined 是唯一精准匹配自定义元素“注册完成且已升级”状态的伪类,需配合 :not(:defined) 占位、含短横线标签名、define 成功及 css 加载时机正确才生效。

:defined 是唯一能卡在自定义元素“注册完成且已升级”这一刻才生效的 CSS 伪类,它不看 JS 加载顺序、不依赖 DOM 渲染时机,只认 customElements.define() 是否真正成功执行。用对了,FOUC(无样式内容闪烁)基本消失;用错了,页面照闪不误。
为什么只写 my-button:defined 还是会闪
因为浏览器在解析到 <my-button></my-button> 标签时,如果 customElements.define('my-button', MyButton) 还没执行,这个标签就是个未知元素,默认 display: inline,样式规则根本不会应用——哪怕你写了 my-button { display: block; },它也无效。
更关键的是:my-button:defined 只在“已升级”后才匹配,但在此之前,元素已经裸着渲染出来了。所以必须主动给“未定义状态”留出占位:
-
my-button:not(:defined)必须写,且要放在my-button:defined之前(否则权重高者覆盖) - 占位样式不能是
visibility: hidden或opacity: 0——它们不占空间,会导致后续内容上移,重排更明显 - 推荐用
display: block; min-height: 48px; background: #f0f0f0;这类有明确尺寸和视觉反馈的占位 - 如果组件最终是
inline行内表现,not(:defined)占位也得设成display: inline-block,保持盒模型一致,避免重排
:defined 失效的三个硬性前提
它不是魔法开关,缺一不可:
- 标签名必须含短横线:比如
my-button合法,MyButton或button都不行 -
customElements.define()必须成功执行:控制台报Failed to define 'my-button': the element name is not valid就永远不匹配 - CSS 必须在
内或至少在注册语句之后加载:若 CSS 放且 JSdefer,Safari 和部分 Chromium 版本大概率错过
开发时可临时加一句 console.log(customElements.get('my-button')),返回构造函数说明已注册成功。
大规模项目里容易被忽略的坑
当组件数超 50 个,每个都靠 :not(:defined) 控制占位,CSS 匹配开销会上升——浏览器得为每个未定义标签做一次生命周期状态检查,尤其在虚拟滚动、动态插入节点等场景下,可能引发 layout thrashing。
- 骨架屏结构必须和最终 DOM 完全一致,否则占位样式再准,渲染后照样重排
-
:defined不解决样式穿透问题:外部h2 { color: red; }依然会影响组件内部文字,需靠 Shadow DOM 或all: initial重置 - 若用了
is=""语法(如<div is="my-button">),Safari 不识别其 <code>:defined状态,必须改用<my-button></my-button>标签形式最常被跳过的一步是:没确认注册语句是否真在样式表加载前执行。哪怕只差几毫秒,
:not(:defined)就来不及接管——这不是浏览器 bug,是规范行为。











