
本文详解如何使用 web components(custom elements api)构建真正符合无障碍标准的自定义 html 元素,涵盖语义增强、键盘导航支持、事件绑定时机优化及 aria 实践,避免常见陷阱如 dom 未挂载即绑定事件。
本文详解如何使用 web components(custom elements api)构建真正符合无障碍标准的自定义 html 元素,涵盖语义增强、键盘导航支持、事件绑定时机优化及 aria 实践,避免常见陷阱如 dom 未挂载即绑定事件。
在现代 Web 开发中,自定义元素(Custom Elements)是构建可复用、封装良好组件的核心能力之一。但仅实现视觉样式与基础交互远远不够——真正的专业实践要求组件具备语义正确性、键盘可操作性、屏幕阅读器兼容性以及生命周期健壮性。以下是一套完整、可落地的实现指南。
✅ 正确的生命周期:connectedCallback 而非 constructor
关键误区在于:constructor 中 DOM 尚未挂载,此时无法安全操作子节点、添加事件监听器或读取 this.innerHTML。必须将 DOM 相关初始化逻辑移至 connectedCallback——该回调在元素被插入文档时触发,是注册事件、渲染 Shadow DOM 或设置初始状态的唯一可靠时机。
<style>
.custom-button {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
outline: none;
}
.custom-button:focus {
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
</style>
<accessible-button label="提交表单">Submit</accessible-button>
<script>
class AccessibleButton extends HTMLElement {
static get observedAttributes() {
return ['label'];
}
constructor() {
super();
// ✅ 仅用于初始化属性、创建 shadowRoot(若需)
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
// ✅ 安全:此时元素已存在于 DOM 中
this.render();
this.setupEventListeners();
}
render() {
const label = this.getAttribute('label') || this.textContent.trim();
this.shadowRoot.innerHTML = `
<button
type="button"
aria-label="${label}"
tabindex="0"
>
${this.innerHTML || 'Button'}
</button>
`;
}
setupEventListeners() {
const button = this.shadowRoot.querySelector('button');
// 支持鼠标点击 + 键盘回车/空格触发
button.addEventListener('click', () => this.handleClick());
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
this.handleClick();
}
});
}
handleClick() {
this.dispatchEvent(new CustomEvent('action', { bubbles: true, composed: true }));
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'label' && oldValue !== newValue) {
this.render(); // 属性变更时重新渲染以同步 aria-label
}
}
}
customElements.define('accessible-button', AccessibleButton);
</script>? 关键无障碍实践(Accessibility Checklist)
- 语义化角色与属性:使用









