必须用 <button> 做标题,因其具备原生键盘交互(空格/回车触发、Tab 导航、自动聚焦),而 <div role="button"> 无法响应空格键且焦点管理易错;标题需设 type="button" 防提交,嵌套内容用 <span>,禁用时同步设 disabled 和 aria-disabled="true";aria-expanded 与 hidden 必须严格同步更新,不可依赖 CSS 类;面板容器须为 role="region" 并配 aria-labelledby 指向按钮 ID;动画宜用 max-height 过渡并保留 overflow: hidden,避免 height: auto 或 display: none。

必须用 <button> 做标题,不能用 <div> + role="button"
屏幕阅读器依赖原生 <button> 的键盘行为(空格/回车触发、自动聚焦、可被 Tab 导航),<div role="button"> 缺少这些能力,视障用户会卡在标题上无法操作。即使加了 tabindex="0",也无法响应空格键,且焦点管理极易出错。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 每个标题必须是语义化
<button type="button">,避免表单意外提交 - 如果标题里要嵌链接或图标,用
<span>包裹,不要破坏<button>的直接子元素结构 - 禁用状态时,设
disabled+aria-disabled="true",只设其一会导致辅助技术误判
aria-expanded 和 hidden 必须同步更新,不能靠 class 名判断状态
屏幕阅读器只读 aria-expanded 的值,不看 .is-open 这类 CSS 类。JS 切换显隐后若忘记更新这个属性,用户听到的永远是“已折叠”,哪怕内容已经展开了。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 每次点击前,先用
getAttribute('aria-expanded') === 'true'读取当前值(别用布尔比较) - 展开时:设
setAttribute('aria-expanded', 'true')+ 移除hidden属性 - 收起时:设
setAttribute('aria-expanded', 'false')+ 添加hidden属性 - 初始加载若默认展开,
aria-expanded="true"和移除hidden必须同时存在
面板容器必须带 role="region" 和 aria-labelledby
仅靠 aria-expanded 不足以让屏幕阅读器把按钮和对应内容关联起来。没有 aria-labelledby,用户展开后可能完全不知道内容在哪,也无法用快捷键跳转。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 每个内容区用
<div role="region" aria-labelledby="btn-id">,其中btn-id是对应按钮的id - 按钮必须有
id,且值与aria-labelledby严格一致(大小写、连字符、空格都不能错) - 禁止多个面板共用同一个
id——NVDA 和 VoiceOver 表现不一致,容易漏读 - 不要用
role="tabpanel",那是 tab 组件专用,手风琴用role="region"
动画不能依赖 height: auto,hidden 比 display: none 更安全
用 height: 0 → height: auto 加 transition 会失败,因为 auto 无法参与过渡计算;而 display: none 会让屏幕阅读器彻底跳过内容,违反可访问性原则。
实操建议:
立即学习“前端免费学习笔记(深入)”;
- 用
max-height过渡:展开前读取scrollHeight,设为max-height值;收起时设回0 - 始终保留
overflow: hidden,否则内容会在动画中途溢出 - 显隐控制只用
hidden属性,配合 JS 同步切换,不要混用display: none - 若内容含
margin或padding,需在max-height中额外预留空间,否则动画结束时布局会跳变
id 对应、一次 aria-expanded 更新,对普通用户无感,但对屏幕阅读器用户就是功能断裂。



















