details标签默认不持久折叠状态,需用localStorage配合data-id为每个标签独立存取open状态,加载时还原、toggle时用hasAttribute判断并保存,动态插入元素需额外初始化。

details 标签的折叠状态默认不持久,刷新就重置 —— 但只要给每个 details 加上唯一 ID,并用 localStorage 存它的 open 属性值,就能稳稳记住。
为什么直接监听 open 事件 + setItem 不够用
很多人写完 details.addEventListener('toggle', () => localStorage.setItem('my-details', details.open)) 就以为搞定了,结果一刷新还是展开的。问题出在两处:
- 页面加载时没读取
localStorage去主动设置details.open = true/false,浏览器按 HTML 默认(没写open就是false)渲染 -
toggle事件触发时,details.open还没更新(它是异步更新的),此时读到的是旧值;得用setTimeout(() => ..., 0)或监听transitionend后再取,更稳妥的是直接用this.hasAttribute('open')
怎么给多个 details 分别记状态
不能全用一个 key,否则互相覆盖。必须为每个 details 提供稳定、可复用的标识符:
- 推荐用
data-id属性,比如<details data-id="faq-1">或<details data-id="section-about"> - 避免用
id属性——它要求全局唯一,而模板里可能重复渲染;data-id更灵活,且不参与 CSS/锚点逻辑 - 键名拼成
details_${id}_open,例如details_faq-1_open,读写都靠这个字符串
完整初始化 + 监听逻辑(原生 JS,无框架)
把这段代码放在 <script> 里,或 DOMContentLoaded 后执行:
立即学习“前端免费学习笔记(深入)”;
document.querySelectorAll('details[data-id]').forEach(details => {
const id = details.dataset.id;
const key = `details_${id}_open`;
const saved = localStorage.getItem(key);
// 加载时还原:有记录就设 open 属性,否则移除(保持默认折叠)
if (saved === 'true') {
details.setAttribute('open', '');
} else {
details.removeAttribute('open');
}
// 点击后保存:用 hasAttribute 判定当前是否展开,更可靠
details.addEventListener('toggle', () => {
const isOpen = details.hasAttribute('open');
localStorage.setItem(key, String(isOpen));
});
});
注意:不要用 details.open = true 赋值,它不触发 DOM 反映;必须用 setAttribute/removeAttribute 才能同步到渲染层。
容易被忽略的边界情况
实际项目里这几个点常被跳过,导致“有时记得、有时不记”:
- 动态插入的
details(比如 AJAX 渲染、Vue/React 组件挂载后)不会被上面的querySelectorAll捕获,得单独调用初始化函数 - 用户手动清空
localStorage或开启隐私模式时,getItem会返回null,String(null) === 'null',所以判断必须写成saved === 'true',而不是!!saved - 如果页面有多个同名
data-id,状态会串,务必保证每个值在当前页唯一



















