选用 data-expanded 控制状态最稳妥,因其语义明确、易追踪持久化、避免 class 冲突、服务端渲染友好、支持 CSS 选择器;需配合 scrollHeight 动态设置 max-height 实现平滑过渡,并注意事件委托与 localStorage 键名上下文隔离。

直接用 data-expanded 控制状态最稳妥,比只靠 class 或 style 更易追踪、更易持久化、也更方便配合 CSS 选择器做样式切换。
为什么选 data-expanded 而不是 class 切换
class 名容易语义模糊(比如 active 可能表示选中/聚焦/展开等多种含义),而 data-expanded 明确表达布尔状态,JS 读取时直接用 el.hasAttribute('data-expanded') 或 el.getAttribute('data-expanded') === 'true' 就能判断,不依赖样式类是否存在。localStorage 存取也更直观:localStorage.setItem('nav_main_expanded', 'true')。
- 避免 class 冲突:多个功能共用同一元素时(如同时有
is-open和is-disabled),data-属性互不干扰 - 服务端渲染友好:服务端可直接输出
data-expanded="true",前端 JS 拿到即用,不用等 DOM 加载完再 patch 状态 - CSS 里能直接写
[data-expanded="true"] .content { max-height: 300px; },无需额外 JS 插入 class
data-expanded 必须配合 max-height 动画,不能用 height: auto
CSS transition 不支持 height: auto,所以必须用 max-height 模拟。但值不能硬写死——内容高度变化大时,设太小会截断,设太大动画拖沓。
- 安全做法:JS 点击时先取目标元素的
scrollHeight,再设为max-height;收起时设max-height: 0,并加overflow: hidden - 收起后建议清除行内
max-height(el.style.maxHeight = ''),否则后续scrollHeight判断可能被旧值干扰 - 别给
details元素设max-height过渡——它原生逻辑和 CSS 动画冲突,Safari 下尤其容易抖动
绑定事件时注意 event.target 和冒泡控制
如果折叠区域里有链接、按钮或表单控件,直接在容器上监听 click,点子元素就会误触发折叠。必须限定触发源,并阻止冒泡。
立即学习“前端免费学习笔记(深入)”;
- 只响应
<button>或带role="button"的元素:if (event.target.matches('.toggle-btn, [data-toggle]')) { ... } - 点击内部链接时,要
event.stopPropagation(),否则父级accordion-item也会跟着收起 - 多级树形结构下,用
event.target.closest('[data-category-id]')找最近的可折叠节点,比遍历所有button更健壮
和 localStorage 联动时 key 命名要带上下文
单纯存 category_expanded 容易冲突,尤其页面有多个同类组件(比如侧边栏菜单 + 笔记分类 + 设置面板)。
- 推荐格式:
localStorage.setItem(`sidebar_${id}_expanded`, isExpanded),其中id来自el.dataset.id - 首次加载时若 key 不存在,默认按折叠处理(
isExpanded = false),避免旧用户看到异常展开态 - 删除某个分类后,对应 localStorage 条目不会自动清理,但无害——下次读取时
getItem返回null,自然走默认逻辑
真正难的不是写 toggle 逻辑,而是让 scrollHeight 测量准、让 max-height 过渡顺、让 data-expanded 和辅助技术(如屏幕阅读器)的状态同步。这三个点没对齐,用户要么看不见动画,要么读不到当前状态,要么点一下展开两层。



















