浏览器对原生标签的保护远比想象中严格——直接重定义 button、canvas 这些标签,会被浏览器无情拒绝。那如果想给按钮加个加载状态、或者让自定义元素继承原生表单能力,该怎么办?唯一合法路径是用 { extends: "button" } 配合 is 属性。独立自定义元素虽然看起来更自由,但原生 API 一个都拿不到,事件、属性、表单能力全得手动桥接。

直接重定义 button、canvas 等原生标签会报错
试着执行 customElements.define("button", ButtonClass)——浏览器会直接甩出错误:Failed to execute 'define' on 'CustomElementRegistry': "button" is not a valid custom element name。这可不是兼容性问题,而是浏览器在解析阶段就把它拦截了。所有已知的 HTML 标签名(button、input、canvas、table 等,大约 20 个)都被硬编码在拒绝名单里。即便你试图改原型链或者动态替换,最终生成的节点依然是 HTMLButtonElement 实例,你的类根本不会被实例化。
扩展现有元素必须用 is 属性 + extends 配置
想让 具备加载状态管理能力?唯一可行路径是组合使用 is 属性和 { extends: "button" }。漏掉任何一环,属性、方法、事件全都会失效。这里的细节一个都不能错:
class LoadingButton extends HTMLButtonElement—— 必须继承对应的内置类,不能只继承HTMLElement。- 构造函数第一行必须是
super(),否则会报Failed to construct 'HTMLButtonElement',而且this.value、this.disabled等全部变成undefined。 customElements.define("loading-button", LoadingButton, { extends: "button" })—— 这里的{ extends: "button" }不可省略。- HTML 中只能写
,写成或者都是无效的。 is的值("loading-button")必须与define()的第一个参数完全一致,大小写、连字符都不能错。
独立自定义元素无法自动继承原生 API
写 看起来更自由,但它的本质是 HTMLElement 实例,没有 click()、focus()、form 关联、value 属性等任何原生按钮能力。这些能力你得手动实现:
- 用
attachShadow({ mode: "open" })封装内部的,再透传事件和属性。 不会自动设置内部的value,必须在attributeChangedCallback里手动同步:this.querySelector("button").value = this.getAttribute("value")。- 如果需要表单参与能力(比如被
form.elements捕获),必须启用formAssociated: true并调用this.attachInternals(),否则reportValidity()和提交值都不可用。 - 不支持
is属性的原生标签(如、)无法用这种方式扩展,只能走独立封装路线。
旧浏览器必须加 polyfill,且加载时机不能错
Chrome 67+、Firefox 63+、Safari 10.1+ 原生支持 v1 规范,但 IE 和旧版 Edge 完全不支持。不引入 polyfill,自定义元素在这些环境里就是普通未知标签,connectedCallback 不触发,customElements.define() 甚至可能静默失败。
- 推荐用官方维护的
@webcomponents/custom-elements,CDN:https://unpkg.com/@webcomponents/custom-elements@1.1.2/custom-elements.min.js。 - polyfill 必须在任何
customElements.define()调用之前加载,放在末尾或开头最稳妥。 - 如果 DOM 已解析完成再加载 polyfill,已存在的自定义标签不会自动升级,需要手动调用
customElements.upgrade()。
真正能拿到 getContext()、value、checkValidity() 这些原生 API 的,只有 is 扩展路径;而独立标签看似灵活,实则每项原生能力都要自己桥接,稍有遗漏就会导致表单失联、焦点异常或验证失效。最容易被忽略的是:浏览器对配置错误几乎不报错,只会安静地当它是个普通元素处理——你得靠 el instanceof HTMLButtonElement 或 el.value !== undefined 主动验证是否真的生效。
