
本文详解如何使用 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)
- 语义化角色与属性:使用