html自定义元素是设计系统底层封装的唯一原生载体,因其提供语义化dom节点、跨技术栈一致性、shadow dom隔离、属性响应式生命周期及可演进的契约稳定性。

HTML自定义元素不是设计系统的“补充工具”,而是其底层封装机制的唯一原生载体——它让设计语言真正落地为可复用、可约束、可演进的 DOM 实体。
为什么不能只靠 CSS 组件库或框架封装?
设计系统的核心诉求是跨技术栈一致性,而 CSS 类名(如 .btn-primary)或 React/Vue 组件本质是运行时抽象,无法被 HTML 解析器识别、无法被无障碍工具直接理解、也无法被搜索引擎语义化索引。自定义元素则不同:<ds-button></ds-button> 是真实存在的 HTML 元素节点,浏览器原生支持其解析、序列化、克隆和事件冒泡路径,这意味着:
- 设计师标注的“按钮”在代码里就是
<ds-button></ds-button>,而不是<button class="ds-button ds-button--primary"></button>这种语义断裂写法 - 当设计规范更新尺寸、圆角或禁用态逻辑时,只需改
ds-button类内部实现,所有页面自动生效,无需 grep 替换 class 名 - 第三方工具(如 Lighthouse、axe)能直接读取
role、aria-属性并校验合规性,因为它是标准 HTMLElement 实例
customElements.define() 是设计系统交付的最终接口
设计系统交付物不该是“一堆 SCSS 文件 + 文档链接”,而应是一个可 import 的模块,其导出必须包含 customElements.define() 调用。这是让组件真正“注册进浏览器”的不可绕过动作:
- 注册必须在 DOM 加载前完成,否则首次渲染会降级为
HTMLUnknownElement,失去所有自定义行为 - 标签名必须含连字符(如
ds-tooltip),否则浏览器拒绝注册,报错Failed to execute 'define' on 'CustomElementRegistry': The name provided ('dstooltip') is not a valid custom element name - 同一个标签名只能注册一次;重复调用会抛
NotSupportedError,所以设计系统包需确保注册逻辑有幂等保护
Shadow DOM 不是可选项,而是设计系统隔离性的物理边界
设计系统组件一旦被多个业务方嵌入,样式污染几乎是必然发生的。仅靠 BEM 或 CSS-in-JS 无法阻止 body * { font-size: 14px } 这类全局规则穿透。而 this.attachShadow({ mode: 'closed' }) 提供的是浏览器级的封装:
- Shadow DOM 内部的
style标签完全不参与全局 CSS 级联,外部样式无法选中其子节点 - 插槽(
<slot></slot>)是唯一受控的内容注入点,业务方传入的 HTML 片段会被严格限制在 slot 分配范围内,不会意外破坏组件结构 - 注意:若使用
mode: 'closed',调试时无法通过el.shadowRoot访问,需改用'open'便于开发期检查
属性与生命周期钩子才是设计系统响应能力的真正来源
设计系统组件不是静态图片,它需要响应尺寸变化、主题切换、状态更新。这些不能靠监听 class 切换实现,而必须依赖标准 API:
-
attributeChangedCallback是响应 HTML 属性变更的唯一可靠入口,比如<ds-input disabled></ds-input>触发禁用逻辑,比监听disabledclass 更准确、更及时 - 必须在
static get observedAttributes()中显式声明要监听的属性,否则attributeChangedCallback不会触发 -
connectedCallback是初始化渲染的最佳时机,但要注意它可能被多次调用(如元素被移出又插入 DOM),因此内部状态初始化逻辑需防重入 - 避免在
constructor中操作 DOM 或访问this.shadowRoot,此时 shadow root 尚未创建
最易被忽略的一点:设计系统里的自定义元素不是“写完就扔”的一次性组件,它的升级路径必须由标签名和属性契约来承载。比如从 ds-button size="large" 升级到支持 size="xl",只要不破坏已有属性含义,业务方代码零修改——这种稳定性,只有原生元素级的契约才能提供。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











