开发者 Marco Campos 在个人博客里做了一次实验:从零重建一个网页按钮。他没有停在“看起来像按钮”这一步,而是照着 MDN、ARIA APG 设计模式和 WCAG 2.2,把原生 <button> 元素默认满足的能力拆成 12 项要求,打算逐条手写实现。结果代码从几行 HTML 膨胀成一个完整的 JavaScript 类,而且只写完了一部分。
这不是浏览器厂商发布的新特性,也不是 Web 标准的重大变化,只是一篇工程博客的思想实验。但它戳中一个前端团队经常忽视的问题:自定义组件想追平原生元素,到底要补多少课,这笔账该谁来买单。
12项契约摊开:从角色到表单API,自定义按钮要补什么
Campos 的起点是自定义标签的空白状态。一个新注册的自定义元素,默认行为跟 <span> 没有区别:不可聚焦,没有语义角色,不响应键盘。他把原生 <button> 的默认能力拆成12项,逐项手工补上。
上面六步是主干,完整清单还有细节。12项摊开看更清楚:
| # | 契约项 | 原生 <button> | 自定义标签起点 |
|---|---|---|---|
| 1 | 语义角色 | 默认 role 为 button | 需手动加 role="button" |
| 2 | 可访问名称 | 文本内容自动被读出 | 图标/空文案需要 aria-label 补 |
| 3 | 键盘可聚焦 | 默认进入 Tab 序 | 需要 tabindex="0" |
| 4 | 鼠标点击 | 默认响应 | 需绑定 click 事件 |
| 5 | 触摸激活 | 默认响应 | 需绑定 touch 事件 |
| 6 | 指针事件 | 默认响应 | 需绑定 pointer 事件 |
| 7 | 键盘激活 | Enter/Space 默认触发 | 需手写 keydown 逻辑 |
| 8 | 禁用状态 | disabled 原生阻断交互 | 要自己实现,还要同步给辅助技术 |
| 9 | 表单关联 | 自动关联最近的 form | 需要 ElementInternals 或手动处理 |
| 10 | 提交 name/value | 原生支持 | 需要额外实现 |
| 11 | 表单校验参与 | 原生参与 :invalid 等状态 | 需要额外实现 |
| 12 | 新平台API(Popover/Invoker Commands等) | 浏览器持续跟进 | 需要自己持续追更 |
前三项解决无障碍问题,对应 WCAG 2.2 的 SC 4.1.2(名称、角色、值)和 SC 2.1.1(键盘可操作)。第9到11项是表单参与,靠的是较新的 ElementInternals API。最后一项——对接 Popover API、Invoker Commands API、Interest Invokers API——不是无障碍规范的要求,是浏览器还在给按钮加新能力,自建实现得自己跟着追。
风险在于,很多团队只做到第1步。加上 role="button" 之后,屏幕阅读器确实会把这个元素读成按钮,但键盘响应、表单提交、disabled 状态不会跟着来。这是12项里最容易被误解的一条:角色变了,契约没变。
原生标签省的不是代码,是维护契约
Campos 实验真正有价值的地方,不是证明写按钮很麻烦,是揭示一个容易被忽视的落差。<button> 默认自带焦点、Enter/Space 激活、表单提交和 disabled 语义,这是浏览器多年演进沉淀下来的跨平台约定。任何自定义标签的默认起点,都更接近一个什么都没有的 <span>。
这个差距还在拉大。Campos 提到原生按钮正在对接 Popover API、Invoker Commands API 和 Interest Invokers API 这些新平台能力。自建实现想保持功能对等,就得跟着浏览器的更新节奏持续追,写完一次不算完工。
原文没有说明具体测试过哪些浏览器和读屏器组合,比如 NVDA、VoiceOver、TalkBack——这是想复现验证的读者需要自己补的部分。Campos 也在文章末尾留了没写完的部分,他的代码更像教学示意,不是能直接上线的完整实现。
对前端和选型负责人,分别意味着什么
对前端开发者,这份清单给了一条清楚的默认规则:能用原生元素,就别用自定义标签重写一遍。真正需要自建控件的场景,通常是原生元素覆盖不到的复杂交互,比如自定义下拉列表、富文本工具栏、多选组合控件。如果确实要自建,12项里键盘、表单、禁用状态这几项不能只做视觉还原就上线。
对负责组件库选型和无障碍验收的技术负责人,这份清单更像一份验收单。采购或评审组件库时,应该把键盘操作、屏幕阅读器朗读、表单提交和禁用状态列进测试项,而不是只看视觉还原度。供应商拿不出这几项的测试记录,验收结论就该往后放一放,不要先上线再补测试。
接下来值得盯的变量,是 Popover API 和 Invoker Commands API 这类新特性铺开之后,自定义按钮跟不跟得上迭代节奏。一个团队如果连当前12项都没补齐,新API大概率也会被搁置。
