应避免在内嵌套role="tablist",因其违反ARIA规范导致可访问性失效;正确做法是将tablist置于details外层或改用自定义手风琴组件,并严格管理aria-hidden、tabindex、aria-selected及状态持久化。

details 里嵌套 tablist 会破坏语义和键盘导航
原生 <details></details> 已自带折叠逻辑和可访问性支持,但它的内容区不是 role="tablist" 的合法容器。若强行在 <summary></summary> 后塞 <div role="tablist">,会导致:屏幕阅读器无法正确识别页签层级、Tab 键跳转顺序错乱、<code>ArrowLeft/ArrowRight 切换失效。
根本原因在于 ARIA 规范要求 tablist 必须是“顶级导航容器”,不能嵌套在交互式元素(如 <details></details>)内部——否则其子项的 role="tab" 会被降级为普通按钮。
- 别用
<details><summary>菜单</summary><div role="tablist">…</div></details> - 如果必须折叠+标签页共存,把
tablist放在<details></details>外层,用 JS 控制整个<details></details>的展开状态与当前激活页签解耦 - 或者直接放弃
<details></details>,改用<div class="accordion"> + 手动管理 <code>aria-expanded和aria-controls用 data-target 关联子标签页时,ID 匹配必须严格一致
常见错误是 JS 查找目标面板时用
document.getElementById(),但 HTML 中id值含空格、中文或以数字开头,导致匹配失败,控制台报TypeError: Cannot read property 'classList' of null。实操要点:
立即学习“前端免费学习笔记(深入)”;
-
data-target的值必须和对应面板的id完全相同(区分大小写、不带#):data-target="faq-1"→<div id="faq-1"> <li>ID 不能以数字开头(<code>id="1-panel"非法),建议统一用id="panel-faq-1" - 避免动态生成 ID 时重复,比如循环中没加唯一索引,导致多个面板共用一个
id,JS 只能操作第一个 - 对当前激活面板:移除
aria-hidden,设tabindex="0" - 对其他所有同级面板:设
aria-hidden="true",设tabindex="-1" - 页签本身也要同步:
role="tab"元素中,当前激活项加aria-selected="true",其余设"false"
切换子标签页时,要同步更新 aria-hidden 和 tabindex
只改
display或max-height不够。若不手动设置aria-hidden="true"和tabindex="-1",隐藏面板仍会被屏幕阅读器读出,键盘用户也能 Tab 进去,造成逻辑混乱。推荐做法(每次切换后执行):
示例片段:
const panel = document.getElementById(targetId);<br>panel.removeAttribute('aria-hidden');<br>panel.setAttribute('tabindex', '0');折叠菜单收起时,正在显示的子标签页状态容易丢失
用户可能在折叠前已切换到第 3 个标签页,但点击父级
<summary></summary>收起后,再展开时默认回到第 1 个——因为原生<details></details>不保存子状态。解决方式不是靠 CSS 或 HTML 属性,而是用 JS 持久化记录:
- 监听
toggle事件,在<details></details>收起前,把当前激活页签的data-tab-id存进localStorage或组件级变量 - 展开后,从存储中读取并触发对应页签的
click,或直接调用切换函数 - 注意:不要用
sessionStorage,它在页面刷新后清空;若需跨会话保留,才用localStorage
这个细节常被忽略——视觉上“收起再展开”似乎只是高度变化,但语义状态其实断开了。
-



















