用 css 类实现按钮更简单高效,仅当需跨技术栈、强样式隔离、内置国际化时才考虑 web component;若使用,必须正确同步 disabled/type/name/value 属性并透传 click 事件及表单 submit 行为。

直接用 <button></button> + CSS 就能做出高可用、可复用的自定义按钮,没必要硬上 Web Component;真要封装成自定义元素,必须处理好 disabled、type、焦点管理、事件透传这四个关键点,否则在表单或可访问性场景下会出问题。
为什么不用 Web Component 写按钮组件
绝大多数项目里,<button class="btn-primary">提交</button> 配合一套 CSS 类就足够了。Web Component 的封装成本高:需要注册 customElements.define()、手动同步属性到内部 <button></button>、处理 shadow DOM 下的样式穿透和伪类(比如 :hover 在 shadow root 里不自动生效)、还要补全 focus()、click() 等方法代理。实际收益极低,反而增加 bundle 体积和调试复杂度。
只有当你要满足以下全部条件时,才值得考虑:
- 按钮需跨多个技术栈(React/Vue/纯 HTML)统一使用
- 必须强制隔离样式,且不允许外部 CSS 覆盖内部状态
- 需要内置国际化文案或动态图标加载逻辑
<my-button></my-button> 必须透传的原生属性和事件
如果真要实现一个自定义按钮元素,它不能只是“长得像按钮”,而必须行为一致。否则在表单中会被忽略、屏幕阅读器无法识别、键盘导航会中断。
必须手动同步的关键属性包括:
-
disabled:要同步到内部<button></button>,并设置tabindex="-1"和aria-disabled="true" -
type:仅支持"button"、"submit"、"reset",且只在包裹<form></form>时才允许submit/reset -
name和value:表单提交时依赖这两个字段,不能丢 -
click事件必须从内部<button></button>冒泡出来,不能被 shadow DOM 截断
示例片段(非完整):
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<button part="button"><slot></slot></button>
`;
}
connectedCallback() {
const btn = this.shadowRoot.querySelector('button');
// 同步 disabled
if (this.hasAttribute('disabled')) {
btn.disabled = true;
btn.setAttribute('aria-disabled', 'true');
btn.tabIndex = -1;
}
// 透传 click
btn.addEventListener('click', () => {
this.dispatchEvent(new Event('click', { bubbles: true, composed: true }));
});
}
}
CSS 自定义按钮更推荐用 :host + part 而非 shadow DOM
想兼顾样式隔离和易用性,可以用 :host 控制组件自身,用 part 暴露内部可定制节点,而不是一刀切进 shadow DOM。
比如这样写:
<my-button variant="danger">删除</my-button>
对应 CSS 可以是:
my-button {
--my-btn-bg: #e74c3c;
}
my-button[variant="danger"] {
--my-btn-bg: #c0392b;
}
my-button::part(button) {
background-color: var(--my-btn-bg);
border: none;
padding: 8px 16px;
border-radius: 4px;
}
好处是:
- 外部仍可通过
my-button:hover::part(button)控制悬停态 - 不需要手动代理
:focus或:active样式 - 开发者可覆盖 CSS 变量快速换肤,比 JS 属性更轻量
最容易被忽略的兼容性坑:表单内 submit 行为
很多人以为只要把 type="submit" 写进自定义元素就能触发表单提交——错。浏览器只认原生 <button type="submit"></button> 或 <input type="submit"> 的提交语义。
如果你的 <my-button type="submit"></my-button> 放在 <form></form> 里,它默认不会提交。必须显式监听 click 并调用 form.requestSubmit(),还要找最近的 <form></form> 元素:
const form = this.closest('form');
if (form && this.type === 'submit') {
form.requestSubmit();
}
注意:requestSubmit() 会触发 submit 事件并走表单验证,比 submit() 更安全;但 IE 完全不支持,得降级处理。
真正难的不是怎么“画”一个按钮,而是让它的行为在任何上下文里都和原生按钮一致——这点连很多 UI 库都做得不彻底。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











