details name 属性实战:原生 HTML 手风琴,不用 JS 也能一次只展开一个

2026-09-13 0 1,007

做 FAQ 页面、做设置面板、做文档侧边导航的时候,”手风琴”这个组件几乎每次都会碰到。它的交互规则很明确:点开一个,原本展开的那个自动收起来。听起来简单,但用 <details> 硬做的话,得写一段监听 toggle 事件的脚本,把其他打开的 <details> 挨个 removeAttribute('open'),还要小心不要触发循环。

这段代码不难写,但它属于那种”明明应该是 HTML 层的事,却偏要用 JS 兜”的负担。而 name 属性就是来终结它的。

两行 HTML,一个互斥手风琴

先看最小可用的例子:

<details name="faq">
  <summary>支持哪些支付方式?</summary>
  <p>微信、支付宝、银联以及货到付款。</p>
</details>

<details name="faq">
  <summary>多久能发货?</summary>
  <p>工作日 16:00 前下单当天发出。</p>
</details>

<details name="faq">
  <summary>支持七天无理由吗?</summary>
  <p>支持,非定制品类都可以。</p>
</details>

打开页面就能用。点第二个的时候,第一个会自动收起来。没有一行 JavaScript。

这里的规则很简单:给一组 <details> 起一个相同的 name,它们就会被浏览器视为”同一组”,同一时刻最多只有一个可以处于展开状态。名字用什么字符串无所谓,只要同一个页面里同一组元素用的一致就行。

它和 radio 的思路很像,但有个关键区别

如果你写过表单,会发现这个机制跟单选按钮的 <input type="radio" name="..."> 特别像。给一组 radio 一个相同的 name,它们就自动互斥。浏览器在更底层的”命名分组”逻辑上是一套东西。

但有一个差异要记住:radio 一旦选中就没法取消,<details> 可以全部收起来

如果你想要”必须有一项展开”的效果,比如 tab 面板那样,得自己监听 toggle 事件:

const group = document.querySelectorAll('details[name="tabs"]');

group.forEach(el => {
  el.addEventListener('toggle', () => {
    if (!el.open) {
      const anyOpen = [...group].some(d => d.open);
      if (!anyOpen) {
        // 全部被关掉了,把刚关的那个重新打开
        el.open = true;
      }
    }
  });
});

这种写法小心中间会有一次视觉抖动。toggle 事件是在状态已经变化之后触发的,所以用户会看到”收起来又立刻展开”的闪一下。更稳的做法是用 beforetoggle,它是变化前触发,可以 preventDefault() 阻止关闭:

group.forEach(el => {
  el.addEventListener('beforetoggle', e => {
    if (e.newState === 'closed') {
      const anotherOpen = [...group].some(
        d => d !== el && d.open
      );
      if (!anotherOpen) e.preventDefault();
    }
  });
});

不过大多数场景下,允许全部收起其实更舒服。用户点开看完了想全收起来,你还拦着不让关,是反常识的。

加开合动画:先把默认行为搞清楚

直接给 details 加 transition 是没用的,因为它在关闭时 content 部分会被设成 display: none,没有中间状态可过渡。

以前要做动画,得自己包一层 wrapper,用 JS 读取 scrollHeight,设置 height: 0 再改成具体像素值,动画结束再改成 auto。这套流程写过的人都知道有多烦。

现在浏览器提供了 ::details-content 伪元素,它代表的是 <details> 里除了 <summary> 之外的那部分内容。给它加过渡就行:

details::details-content {
  block-size: 0;
  overflow: hidden;
  transition:
    block-size 280ms ease,
    content-visibility 280ms allow-discrete;
}

details[open]::details-content {
  block-size: auto;
}

关键在 allow-discrete 上。它让 content-visibility 这种离散属性也能参与过渡——浏览器会等到 block-size 动画播完,才真正把内容设为不可见。如果不加这一句,内容会在动画刚开始的瞬间就消失。

还有一个细节是 block-size: auto。CSS 里 auto 到具体数值本来是没法插值的,这么多年一直是动画做不了灵活高度的老大难。Chrome 从 129 开始支持了 interpolate-size: allow-keywords,写了它之后,浏览器就会用 calc-size 的方式给 auto 一个计算出来的值,然后做插值。

写法是在根元素上加一行:

:root {
  interpolate-size: allow-keywords;
}

有了这一行,前面那段 block-size: auto 的动画才能真正跑起来。不加的话动画会直接跳到终点,中间没有插值过程。

完整案例:一个产品 FAQ 页面

把上面的东西拼起来,做一个带点样子的 FAQ 区域。

<section class="faq">
  <h2>常见问题</h2>

  <details name="faq">
    <summary>支持哪些支付方式?</summary>
    <p>微信支付、支付宝、银联,企业客户可以通过对公转账。</p>
  </details>

  <details name="faq">
    <summary>订单多久发货?</summary>
    <p>工作日 16:00 前下单当天发出,其余次日发出。节假日顺延。</p>
    <p>偏远地区可能额外需要 1 到 2 天。</p>
  </details>

  <details name="faq">
    <summary>支持七天无理由吗?</summary>
    <p>支持。非定制品类、不影响二次销售的情况下都可以。</p>
  </details>

  <details name="faq">
    <summary>发票怎么开?</summary>
    <p>下单时勾选开票,电子发票会在发货后 24 小时内发到邮箱。</p>
  </details>
</section>

样式部分(省略视觉细节,只保留和交互相关的):

:root {
  interpolate-size: allow-keywords;
}

details {
  border-block-end: 1px solid #e6e6e6;
}

summary {
  cursor: pointer;
  padding: 14px 0;
  list-style: none;
  position: relative;
}

summary::-webkit-details-marker {
  display: none;
}

summary::after {
  content: "+";
  position: absolute;
  right: 0;
  transition: transform 220ms ease;
}

details[open] summary::after {
  transform: rotate(45deg);
}

details::details-content {
  block-size: 0;
  overflow: hidden;
  transition:
    block-size 280ms ease,
    content-visibility 280ms allow-discrete;
}

details[open]::details-content {
  block-size: auto;
}

这里面有几处值得展开。

list-style: none::-webkit-details-marker 这两行是为了干掉浏览器默认那个三角形。Chrome、Safari 用 ::-webkit-details-marker,Firefox 用 list-style,两个都写才能覆盖干净。

summary::after 做加号。 转 45 度就成了叉,语义上比三角更直观——展开代表”可以关掉了”。这个动画跟 details-content 的时长配一下会更协调,我这里都用了 220 到 280 毫秒之间,看起来比较统一。

不要给 summary 设 display: flex 这个坑遇到过一次。设了之后,Safari 会丢失点击区域的语义,某些情况下点击不响应。用绝对定位的伪元素更保险。

适合 FAQ,也适合做设置面板

手风琴不只在 FAQ 上有用,设置页也经常需要这种结构。比如:”账户安全”、”通知偏好”、”隐私设置”三个分组,同一时刻只展开一个,视觉上清爽。

<details name="settings">
  <summary>账户安全</summary>
  <label>修改密码</label>
  <label>绑定手机</label>
  <label>二次验证</label>
</details>

<details name="settings">
  <summary>通知偏好</summary>
  <label><input type="checkbox">短信通知</label>
  <label><input type="checkbox">邮件通知</label>
</details>

这种情况下 name 的作用就更明显了——用户点开一个分组去改设置,另一个自动收起来,不用手动去关。而且关闭的动画和打开的动画是同步执行的,视觉上很舒服。

我一般会在这个场景下补一句 details:first-of-type 让它默认展开,毕竟让用户看到空白的三个标题有点别扭。

几个踩过的坑

同一组 name 不能跨上下文复用。 这点文档里没写得很显眼,但浏览器确实按”表单树”来划分——如果你把一组 <details name="x"> 放在一个 <form> 里,另一组放在 form 外面,它们其实不会互斥,即使 name 一模一样。想要互斥就往同一个父级下放。

动态改变 name 会反应得很奇怪。 如果你用 JS 改 details.name,浏览器不会立即重新分组,需要整个 DOM 重新计算一次。生产环境的做法是:用 removeAttribute('name') 把元素从组里摘出来,改完结构之后再重新 setAttribute('name', ...)。或者干脆不要动 name,改用 CSS 控制可见性来规避。

iOS Safari 支持得晚一点。 details name 大概在 Safari 17.2 才开始支持(2023 年底),跨文档到 2024 年才逐渐铺开。::details-contentinterpolate-size 更晚,Safari 26(2025 年)才补上。如果你的产品还有相当一部分旧设备用户,做好降级——不支持的时候浏览器会表现得像没有 name,所有 <details> 都能同时打开。功能不丢,只是体验差一点。

打印样式记得处理。 FAQ 页有时候会被用户打印下来,默认情况下 details 没展开的内容是不会打印的。加一段 @media print { details { display: block; } details::details-content { block-size: auto; } } 就能把全部内容打出来。这个在给客户的 PDF 里经常需要。

不要给 <details> 套一个 <dialog> 这两个东西的语义有点冲突,一个表示”可折叠的摘要内容”,另一个表示”模态窗口”。硬套会带来辅助技术识别混乱。

什么时候不该用 details 手风琴

虽然这套东西很好用,但也不是所有折叠场景都能上。

如果你的内容需要保持 DOM 存在(比如图表要持续更新、视频要一直播放),用 <details> 就不合适,因为在关闭之后 content-visibility 会把内容设为不可见,一些性能敏感的逻辑会受影响。

另外 <details> 天生是”块级”的语义,如果你要做的是横向的 tab 面板切换,用 <details> 是有点勉强的。虽然加 CSS 也能做出来,但辅助技术读起来会觉得怪。这种情况用 ARIA 的 tablist 更合适。

至于嵌套的 <details>,现在浏览器也支持了,但你要留意一下里面的 details 是不是也该参与同一个 name。默认是隔离的,外层打开不会影响内层,这是对的行为。

收尾

过去这些年,很多”交互组件”都默认是要用 JS 写的。手风琴、tab、模态框、弹出层,每一个都得写一段监听逻辑、管理一堆状态。但浏览器近几年的动作很明确——把这些通用的、模式固定的交互,收回到 HTML 和 CSS 的原生能力里。

<details name> 是这套动作里比较小而美的一块。写一个 FAQ 页面,两行 HTML 加一段动画 CSS,一百行 JS 就这么省掉了,而且行为更接近浏览器的”确定行为”——不会因为你的状态管理写得有 bug 而漏掉某个边界情况。

如果手上正好有几个用 JS 手写的手风琴组件,可以考虑改一份试试。改完之后回过头看,会有点”以前怎么忍的”那种感觉。

details name 属性实战:原生 HTML 手风琴,不用 JS 也能一次只展开一个
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 html details name 属性实战:原生 HTML 手风琴,不用 JS 也能一次只展开一个 https://www.taomawang.com/web/html/2753.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务