用data-tab而非id可避免多tabs组件id冲突,通过data-tab与data-tab-panel语义映射精准定位;hidden属性比display:none更可靠,兼顾无障碍与资源管理;须同步更新aria状态、焦点及role属性,并用事件委托确保动态内容生效。

为什么用 data-tab 而不是 id 关联标签和面板
页面里多个 Tabs 组件复用时,id 会冲突——比如两个 <div id="panel-home">,<code>document.getElementById() 只返回第一个,后续切换必然失效。用 data-tab 建立语义映射,按钮写 data-tab="home",面板写 data-tab-panel="home",JS 用 querySelectorAll('[data-tab-panel="' + target + '"]') 精准定位,同一份模板可插入任意次数。
常见错误包括:data-tab="Home "(尾部空格)、大小写不一致("products" vs "Products"),都会导致匹配失败;混用 id 和 data-tab-panel 会让逻辑分支混乱,建议二选一,全程统一。
hidden 属性比 style.display = "none" 更可靠
hidden 不只是隐藏,还会暂停内部 <video></video>、<iframe></iframe>、<audio></audio>,自动处理焦点释放与读屏器跳过,语义和行为都更符合无障碍规范。而 display: none 仅脱离渲染流,资源仍在后台加载,表单输入可能丢失,且不触发任何状态清理。
实操要点:
- 初始面板中,仅默认激活项不加
hidden,其余必须显式加上,否则首屏白屏 - 切换时只操作
p.hidden = true/p.hidden = false,不要混用aria-hidden或style.display - 若需动画,配合 CSS:
.tab-pane:not([hidden]) { opacity: 1; transition: opacity 0.2s; },JS 仍只管hidden
必须手动管理键盘焦点和 ARIA 状态
点击 tab 按钮后内容变了,但按 Tab 键焦点还停在旧按钮上?这不是 bug,是没调用 .focus()。ARIA 规范要求:激活某 tab 后,焦点必须落在该按钮上;若面板内有输入框,还应进一步 targetPanel.querySelector('input, textarea, [contenteditable]').focus()。
同步更新以下三项,缺一不可:
- 激活按钮设
aria-selected="true",其他设"false" - 对应面板移除
hidden,其他全部设hidden - 外层容器必须有
role="tablist",每个按钮有role="tab",面板有role="tabpanel"
漏掉 role="tablist" 是键盘用户完全无法进入 tab 区域的最常见原因。
事件绑定要防 DOM 未就绪和动态插入
脚本放在 里直接执行,querySelectorAll 会返回空列表——DOM 还没解析完。更麻烦的是,如果 tab 按钮由 JS 异步渲染(如 fetch 后插入或 Vue 的 v-for),静态遍历会漏掉新节点。
两种稳妥方案:
- 包在
document.addEventListener('DOMContentLoaded', () => { ... })里 - 用事件委托:
tabsContainer.addEventListener('click', e => { if (e.target.matches('[data-tab]')) { ... } }),容器监听,不依赖节点是否已存在
别用 onclick 写在 HTML 里,既难维护又绕过事件委托机制,动态插入后完全失效。











