按钮点击切换展开与收起,常出现在常见问题、折叠面板、移动端菜单等场景中。核心逻辑只有一句:点击按钮时,给目标内容切换一个表示展开状态的类名。不过如果只做这层切换,页面会瞬间变化,缺少过渡,而且辅助技术也无法感知状态。下面从最基础的结构开始,逐步加入动画和可访问性处理。

基础实现:用类名控制面板显示
先确定HTML结构。使用<button>作为触发器,面板用<div>承载内容。按钮放在面板前面,方便用户按顺序阅读。面板默认收起,对应一个.is-open类名来控制展开状态。
<div class="accordion-item">
<button type="button" class="accordion-trigger" id="triggerOne" aria-expanded="false" aria-controls="panelOne">
展开内容
</button>
<div class="accordion-panel" id="panelOne" role="region" aria-labelledby="triggerOne">
<div class="accordion-inner">
这里是面板里的详细内容,可以包含文字、列表、图片等。
</div>
</div>
</div>
面板内层再加一个.accordion-inner并不是多余的。后续做高度动画时,这个内层用来防止内容在折叠过程中被挤压变形。JavaScript只需要监听按钮点击,在面板类名和按钮属性之间保持一致。
const trigger = document.getElementById('triggerOne');
const panel = document.getElementById('panelOne');
trigger.addEventListener('click', function () {
const isOpen = panel.classList.toggle('is-open');
trigger.setAttribute('aria-expanded', String(isOpen));
});
这里classList.toggle返回布尔值,表示切换后类名是否存在。根据这个值设置aria-expanded,能让屏幕阅读器知道当前展开状态。如果不用toggle的返回值,也可以先读取类名再设置属性。两种方式结果一样,但返回值的写法更简洁。
基础逻辑完成后,可以给面板设置默认样式。比如外层overflow: hidden,默认高度为0,展开时由子元素撑开。但直接设置高度为auto不会产生过渡动画,这是后面需要解决的问题。
加入CSS过渡动画:让高度变化变平滑
CSS中的height: 0到height: auto无法直接过渡,因为浏览器无法在自动高度与固定值之间做插值。传统做法是用固定max-height,例如从0过渡到200px或500px。这种方案简单,但固定值不好维护,内容如果超出预设高度就会被截断。
更稳妥的方案是使用grid-template-rows。把面板外层设为display: grid,默认行高为0fr,展开后变为1fr。CSS Grid的fr单位可以参与过渡,这样浏览器会自动计算子内容高度,不再需要写死像素值。
.accordion-panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease;
}
.accordion-panel.is-open {
grid-template-rows: 1fr;
}
.accordion-inner {
overflow: hidden;
min-height: 0;
}
内层overflow: hidden的作用是折叠时裁掉超出的内容,min-height: 0则避免Grid子项在收缩时保持最低高度。这个方案在现代浏览器中支持良好,不需要引入额外库。动画时长0.3秒比较适合展开收起,太快显得仓促,太慢则让用户等待。
如果项目需要兼容较旧浏览器,可以退回到max-height方案。把面板默认max-height: 0,展开时设置一个足够大的值。缺点是过渡时间会与实际高度不完全匹配,因为从0到500px过渡时,内容只有200px高的部分会较快完成动画,后面会出现停顿感。所以能在现代浏览器中使用grid-template-rows时,优先选择它。
完善无障碍语义与键盘交互
一个<button>天然支持回车和空格键触发,不需要额外监听键盘事件。这是使用原生按钮而不是<div>的重要原因。如果因为样式原因想用<div>模拟按钮,就需要补充tabindex、role="button"以及键盘事件,容易出错且语义更差。
状态属性方面,aria-expanded必须放在按钮上,表示按钮控制的区域是否展开。aria-controls指向面板ID,告诉辅助技术这个按钮控制哪个区域。面板上的role="region"和aria-labelledby可以让用户在导航区域时知道这是由哪个按钮命名的折叠区。
还有一点容易被忽略:当面板收起时,如果它仍然存在于DOM中,屏幕阅读器可能还会读到其中的内容。对于严格的折叠场景,可以配合visibility与过渡时间来控制。例如面板关闭后延迟0.3秒设置visibility: hidden,展开时先恢复可见再触发动画。这个细节需要额外的JS定时器,适合对无障碍要求较高的项目。
trigger.addEventListener('click', function () {
const isOpen = panel.classList.toggle('is-open');
trigger.setAttribute('aria-expanded', String(isOpen));
if (isOpen) {
panel.style.visibility = 'visible';
} else {
setTimeout(function () {
panel.style.visibility = 'hidden';
}, 300);
}
});
这段代码在类名切换后立即调整visibility。展开时马上可见,收起时等动画结束后再隐藏。注意定时器时长要与CSS过渡时间一致。如果项目中多处使用,建议把300这个值提取为配置项,减少维护成本。
封装通用函数并处理多个面板
页面中通常不止一个折叠项。可以为每个按钮单独写监听,但更好的做法是使用事件委托。把监听器挂在外层容器上,通过event.target.closest找到按钮,再根据按钮关联的aria-controls定位面板。
<div class="accordion-list">
<div class="accordion-item">
<button type="button" class="accordion-trigger" aria-expanded="false" aria-controls="panelA">标题一</button>
<div class="accordion-panel" id="panelA"><div class="accordion-inner">内容一</div></div>
</div>
<div class="accordion-item">
<button type="button" class="accordion-trigger" aria-expanded="false" aria-controls="panelB">标题二</button>
<div class="accordion-panel" id="panelB"><div class="accordion-inner">内容二</div></div>
</div>
</div>
JavaScript采用事件委托时,只需要一个监听器。点击按钮后查找其对应的面板,完成类名和aria-expanded更新。这样后续动态添加新面板也不需要重新绑定事件。
const list = document.querySelector('.accordion-list');
list.addEventListener('click', function (event) {
const trigger = event.target.closest('.accordion-trigger');
if (!trigger) return;
const panelId = trigger.getAttribute('aria-controls');
const panel = document.getElementById(panelId);
if (!panel) return;
const isOpen = panel.classList.toggle('is-open');
trigger.setAttribute('aria-expanded', String(isOpen));
});
如果希望实现互斥展开,即打开一项时自动收起其他项,可以在展开前先遍历容器内所有面板,移除除当前面板外的is-open类,并同步更新对应按钮的aria-expanded。这个逻辑很容易抽象成配置项accordion: true。目前代码默认独立展开,适合多数内容列表。
最后要注意按钮文本变化。很多实现会在展开后把按钮文字改为“收起”,收起后改回“展开”。这属于视觉反馈的一部分,但无障碍角度更建议使用aria-expanded表达状态,按钮文本保持稳定,避免屏幕阅读器重复播报“展开”和“收起”造成困惑。如果确实要改文字,可以同时保留一个视觉上隐藏的状态说明,或者依靠图标旋转提供反馈。
常见问题与优化建议
频繁点击按钮时,过渡会反复触发。如果使用max-height方案,会遇到动画中断后高度跳变的问题。grid-template-rows方案本质上也是过渡,但不会出现固定高度截断内容。若需要提高响应速度,可以设置transition-duration较短,或使用prefers-reduced-motion媒体查询关闭动画。
@media (prefers-reduced-motion: reduce) {
.accordion-panel {
transition: none;
}
}
系统开启“减少动态效果”的用户会跳过动画,直接显示最终状态。这个媒体查询虽然简单,却能覆盖一部分对动画敏感的用户。如果在JS中设置了visibility延迟,也要在关闭动画时相应调整定时器,否则可能出现面板已隐藏但视觉上不可见的问题。可以在CSS中保留0秒过渡,并通过检查matchMedia来决定是否使用定时器。
另一个常见问题是内容中存在图片或异步加载数据,导致面板高度在展开后发生变化。使用grid-template-rows时,子内容高度变化会被Grid自动重新计算,过渡中的高度也会随之调整。传统max-height方案则需要等待内容加载完成后再设置一次最大高度,体验较差。这也是推荐现代Grid方案的原因之一。
总体来看,按钮控制展开收起状态并不复杂,但要兼顾动画流畅度和无障碍语义,需要在结构、样式和脚本三方面配合。优先使用原生<button>,用classList.toggle维护状态,用grid-template-rows完成高度过渡,再补充aria-expanded等属性。这样实现出的折叠面板在普通用户和辅助技术用户手中都有稳定表现。
按钮展开收起JavaScriptCSS修改时间:2026-10-06 01:53:11