
常用网页特效代码示例:制作点击展开的折叠菜单
折叠菜单(也常称为手风琴菜单)把一组内容收起,用户点击标题后再查看详情。它适合收纳次级导航、常见问题、设置选项、较长的说明内容等,尤其适用于页面纵向空间有限、信息需要分组呈现的场景。
折叠并不意味着内容可以随意隐藏:重要信息不应只在用户发现某个按钮后才有机会被看到;导航项也要让人知道它们是可展开的。实现时还要同步处理键盘操作、屏幕阅读器状态和重复点击。下面先从一个不依赖框架、可直接运行的基础示例开始。
一、可直接运行的基础示例
将下面代码保存为 accordion.html,用浏览器打开即可测试。示例包含三个独立条目:点击某个标题只会切换对应内容,不会自动关闭其他条目。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>点击展开的折叠菜单</title>
<style>
* {
box-sizing: border-box;
}
body {
max-width: 760px;
margin: 40px auto;
padding: 0 16px;
color: #202124;
font: 16px/1.6 system-ui, sans-serif;
}
.accordion-item {
margin-bottom: 10px;
border: 1px solid #d8dde5;
border-radius: 8px;
overflow: hidden;
}
.accordion-button {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border: 0;
background: #f6f8fa;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.accordion-button:hover {
background: #edf2f7;
}
.accordion-button:focus-visible {
outline: 3px solid #2878d0;
outline-offset: -3px;
}
.accordion-icon {
margin-left: 16px;
transition: transform 180ms ease;
}
.accordion-button[aria-expanded="true"] .accordion-icon {
transform: rotate(180deg);
}
.accordion-panel {
padding: 0 16px 16px;
}
/* 确保隐藏属性不会被其他 display 样式意外覆盖 */
.accordion-panel[hidden] {
display: none !important;
}
@media (prefers-reduced-motion: reduce) {
.accordion-icon {
transition: none;
}
}
</style>
</head>
<body>
<h1>帮助中心</h1>
<section class="accordion" aria-label="帮助主题">
<div class="accordion-item">
<h2>
<button
class="accordion-button"
id="question-1"
type="button"
aria-expanded="false"
aria-controls="answer-1">
如何修改账户资料?
<span class="accordion-icon" aria-hidden="true">⌄</span>
</button>
</h2>
<div
class="accordion-panel"
id="answer-1"
role="region"
aria-labelledby="question-1"
hidden>
<p>登录后进入账户设置,可以修改昵称、头像和联系信息。保存后请确认页面显示了更新结果。</p>
</div>
</div>
<div class="accordion-item">
<h2>
<button
class="accordion-button"
id="question-2"
type="button"
aria-expanded="false"
aria-controls="answer-2">
支持哪些浏览器?
<span class="accordion-icon" aria-hidden="true">⌄</span>
</button>
</h2>
<div
class="accordion-panel"
id="answer-2"
role="region"
aria-labelledby="question-2"
hidden>
<p>示例使用基础 HTML、CSS 和 JavaScript。请根据项目需要,在目标浏览器中进行实际测试。</p>
</div>
</div>
<div class="accordion-item">
<h2>
<button
class="accordion-button"
id="question-3"
type="button"
aria-expanded="false"
aria-controls="answer-3">
如何联系支持团队?
<span class="accordion-icon" aria-hidden="true">⌄</span>
</button>
</h2>
<div
class="accordion-panel"
id="answer-3"
role="region"
aria-labelledby="question-3"
hidden>
<p>请使用产品提供的联系渠道,并在描述问题时附上必要的背景信息。</p>
</div>
</div>
</section>
<script>
document.querySelectorAll(".accordion-button").forEach((button) => {
button.addEventListener("click", () => {
const panelId = button.getAttribute("aria-controls");
const panel = document.getElementById(panelId);
// 找不到对应内容时不继续操作,避免访问空对象。
if (!panel) return;
const isExpanded = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", String(!isExpanded));
panel.hidden = isExpanded;
});
});
</script>
</body>
</html>
二、代码中几个关键点
1. 用按钮承载点击行为
展开标题使用原生 <button>,而不是只给普通文字绑定点击事件。按钮默认支持键盘焦点,并可通过 Enter 或空格激活;type="button" 则能避免按钮放在表单中时意外提交表单。不要用只有鼠标才能操作的 div 代替按钮,除非你愿意自行补齐键盘交互、焦点管理和语义。
2. 用 ARIA 属性表达状态和关联关系
aria-controls 指向被控制内容的 ID,aria-expanded 表示当前展开状态。状态值必须随内容的显示或隐藏同步更新:"true" 表示展开,"false" 表示收起。内容区域上的 aria-labelledby 则把区域名称关联到相应按钮,role="region" 可用于把内容标记为区域。
这里的 role="region" 不是所有场景的必选项。页面中有大量条目时,为每一项都创建区域地标可能增加屏幕阅读器导航负担;可以根据内容数量和页面结构决定是否保留。无论是否使用它,按钮和内容之间的 ID 关联都要正确。
3. 用 hidden 控制内容是否显示
示例给收起的内容加上 hidden,展开时移除它。该属性不仅影响视觉呈现,通常也会让内容不参与页面布局及辅助技术的阅读。CSS 中额外声明 .accordion-panel[hidden] { display: none !important; },是为了防止项目里其他针对面板的 display 规则覆盖隐藏效果。
按钮状态和面板状态应当成对更新,不能只把内容藏起来,却让 aria-expanded 仍显示为展开;反过来,只更改 ARIA 属性也不会自动隐藏内容。
4. 为什么这里没有给面板加高度动画
示例优先采用简单、稳定的显示与隐藏方式,只让图标旋转。CSS 不能直接对 display: none 到 display: block 做传统的高度过渡;常见的 max-height 动画需要预设最大值,内容高度超过预设值时可能被裁剪,远小于预设值时动画速度也会显得不自然。
如果动画对产品体验确实重要,可以单独设计动画实现,并明确处理收起时的可访问性状态、内容高度变化和用户的减少动态效果偏好。不要为了动画而让已收起内容仍能被 Tab 键聚焦,或让屏幕阅读器继续读到本应关闭的内容。对于帮助文档和普通菜单,先保证状态正确,再决定是否增加动画。
三、改成“同一时间只展开一项”
有些 FAQ 或侧边导航希望展开一项时自动关闭其他项。此时可以在点击当前按钮前,把同一组中的其他条目全部收起。下面的脚本可替换基础示例中的脚本,适用于每个按钮都位于同一个 .accordion 容器内的结构:
<script>
document.querySelectorAll(".accordion").forEach((accordion) => {
accordion.addEventListener("click", (event) => {
const button = event.target.closest(".accordion-button");
// 忽略容器内不是折叠按钮的点击。
if (!button || !accordion.contains(button)) return;
const panelId = button.getAttribute("aria-controls");
const panel = document.getElementById(panelId);
if (!panel) return;
const willExpand = button.getAttribute("aria-expanded") !== "true";
accordion.querySelectorAll(".accordion-button").forEach((otherButton) => {
const otherPanelId = otherButton.getAttribute("aria-controls");
const otherPanel = document.getElementById(otherPanelId);
if (!otherPanel) return;
const isCurrent = otherButton === button;
const expanded = isCurrent && willExpand;
otherButton.setAttribute("aria-expanded", String(expanded));
otherPanel.hidden = !expanded;
});
});
});
</script>
这种写法使用事件委托:监听容器上的点击,再从事件目标向上查找按钮。它也适合条目由 JavaScript 后续添加的情况,但动态添加的按钮仍需符合约定的类名和 aria-controls 关系。请勿把多组菜单都放进一个监听范围不清的选择器中,否则一组菜单的点击可能误关闭另一组。
四、关键边界与常见错误
- 按钮和面板的 ID 必须一一对应。 每个
id在整张页面中应唯一;按钮的aria-controls必须与实际面板 ID 完全一致。复制条目时只改了标题,却忘记改 ID,可能导致点击错误内容。 - 不要用链接伪装展开按钮。 如果点击只是开合内容,优先使用按钮。如果确实是导航链接,就让链接完成导航,不要把链接的默认行为和展开行为混成一个难以预期的操作。
- 不要依赖图标表达状态。 箭头旋转只是视觉提示,不能替代按钮文字和
aria-expanded。示例把装饰图标设为aria-hidden="true",避免屏幕阅读器将其作为额外文字朗读。 - 不要误用表单按钮默认类型。 在
<form>内,省略type的按钮通常会按提交按钮处理。展开菜单时显式设置type="button",可以避免意外提交。 - 注意焦点和动态插入内容。 收起面板时,正常情况下焦点仍停留在触发按钮上。若业务逻辑会从面板内部触发关闭,应避免把焦点留在已隐藏的元素中;必要时先将焦点移回对应按钮。
- 键盘操作至少要能完成开合。 使用原生按钮即可获得常规 Tab、Enter 和空格交互。方向键、Home、End 等快捷键属于特定交互模式的增强项,并非所有折叠菜单都必须实现;如果实现,应一并设计焦点移动规则并进行键盘测试。
- 不要把关键内容藏得太深。 搜索引擎是否以及如何处理折叠内容,取决于页面实现与抓取方式,不能把折叠等同于可靠的搜索优化手段。对价格、重要限制和关键操作提示等内容,应考虑默认展示或提供清晰的发现路径。
五、安全与参数边界
上面的实现只读取按钮上的 aria-controls 字符串,再通过 document.getElementById() 查找现有元素;示例没有把用户输入拼进 HTML,也没有调用 innerHTML。折叠菜单本身不构成安全机制,不能用它来保护私密内容、权限信息或敏感操作:页面已经交付给浏览器的内容,通常仍可能被用户查看。真正的访问控制必须由服务端在数据读取和操作接口处执行。
如果面板内容来自用户输入或外部数据,不要直接使用 innerHTML 将未经处理的字符串解析成 HTML。需要展示纯文本时,优先使用 textContent;确实要渲染富文本时,应使用经过审查的 HTML 清理方案,并结合应用的安全策略。客户端把面板设为隐藏,不会消除注入风险。
示例没有设置条目数量、ID 长度或展开内容大小等硬编码限制,因为这些限制应由具体产品需求和数据模型决定。实际项目需要关注的是:ID 不重复、引用可解析、DOM 规模合理,以及外部内容经过适当处理。不要把示例脚本中的容器类名、按钮类名当成安全校验,也不要把自定义 ARIA 属性当作数据校验规则。
六、测试清单
- 用鼠标点击标题,确认对应内容只切换一次,图标和展开状态同步。
- 使用 Tab 移动到按钮,再按 Enter 或空格,确认键盘也能展开与收起。
- 检查初始收起状态:面板不可见,按钮的
aria-expanded为"false"。 - 复制一个菜单条目,确认页面没有重复 ID,按钮仍控制正确的面板。
- 在表单附近测试按钮,确认点击展开不会触发表单提交。
- 用浏览器开发者工具检查控制台错误,并在需要支持的浏览器和屏幕阅读器环境中验证实际体验。
一个可靠的折叠菜单,重点不是让元素“看起来会动”,而是让视觉状态、键盘操作、辅助技术状态和实际内容保持一致。先实现语义清楚、状态同步的基础版本,再按真实需求加入单项展开、动画或动态内容处理,通常更容易维护,也更不容易引入难以发现的交互问题。



