details 元素现代实战:name 手风琴与 ::details-content 展开动画

2026-09-26 0 616

给一家做企业培训的客户改 FAQ 页面时,遇到一个挺有代表性的事儿。

原来的 FAQ 用的是 <details>,一个问答一个块,写起来特别省事,不用 JS 也不用 ARIA。产品提了个需求:展开一个问题的答案时,之前展开的那个要自动收起来,保持页面清爽。

当时第一反应是写 JS:给每个 <details> 绑 toggle 事件,展开之后遍历其他兄弟节点把它们都关掉。代码不到二十行,但有几个藏起来的麻烦。

用户在输入框里打字快,一次点击可能会触发两三次 toggle,如果处理不当会出现”打开了两个”的瞬间。键盘用户用空格展开的时候,toggle 事件的触发顺序和鼠标点击不同。还有 SSR 场景下,如果初始 HTML 里已经有 open 属性的元素,事件绑定的时机和浏览器原生状态的同步会错位——初期内容闪一下才收到通知。

就在琢磨怎么把这些边界都处理干净的时候,同事提醒了一句:details 元素本身就有 name 属性,专门解决这个问题。当时愣了一下,仔细一查,发现这个特性已经落地好几年了,只是平时不太关注。

这篇文章把 <details> 这两年的两块新能力整理一遍:name 属性和 ::details-content 伪元素。前者让手风琴不用 JS,后者让展开收起终于有了动画入口。

一、name 属性:原生手风琴

用法很简单:

<details name="faq">
  <summary>你们的课程可以开发票吗?</summary>
  <p>可以的。付款完成后在订单详情页申请,电子发票会在两个工作日内发到你的邮箱。</p>
</details>

<details name="faq">
  <summary>课程有效期是多久?</summary>
  <p>自购买之日起一年内可以无限次回看。</p>
</details>

<details name="faq">
  <summary>支持换课吗?</summary>
  <p>开课七天内未观看超过 10% 的,可以申请换到任意同价位课程。</p>
</details>

三个 details 用同一个 name,行为就变成了:同一组里最多只有一个处于展开状态。展开第二个的时候,第一个自动收起。

这个机制和单选按钮的 name 是同一个思路——用名字把多个独立的元素编进同一个组,让它们知道彼此的存在。

几个细节需要注意。

name 是分组,不是关联

name 的值是字符串,同一文档里相同 name 的 details 属于同一组。这一点和 <input type="radio"> 完全一致。

但是同名的 details 不需要放在同一个父元素里。下面这种结构也是有效的:

<section>
  <details name="solo"><summary>A</summary><p>...</p></details>
</section>
<aside>
  <details name="solo"><summary>B</summary><p>...</p></details>
</aside>

两个元素在 DOM 里隔着老远,但只要 name 一样,就会互斥。写的时候要注意别不小心把不该关联的两块写了同样的 name,这在大型页面里确实容易发生。

只能在展开方向上互斥

需要注意 name 只处理”展开”的互斥,不处理”关闭”的逻辑。也就是说,展开 A 的时候 B 会被关掉,但手动关掉 B 的时候 A 不会展开。这一点符合手风琴的交互预期,因为用户主动关闭一个面板的时候,通常不希望另一个面板自己弹出来。

和单选按钮的对比

有人会问:<details name> 和用 <input type="radio"> 加 <label> 实现的手风琴有什么不同。

主要的区别在语义。radio 加 label 的方案里,每个面板对应的其实是一个表单控件,页面在语义上会被识别成”有一堆选项可供选择”,而不是”有一些可以展开的内容”。对屏幕阅读器用户来说,前者意味着”这里是一组二选一的选项”,后者意味着”这里有可折叠的说明内容”,感受是不同的。

<details> 天生表达的就是折叠内容,只是借用了 name 这个机制做互斥。语义更准确,也不需要额外的 ARIA。

二、::details-content:给展开加上动画

另一个问题是动画。

给 <details> 加展开动画一直很别扭。因为 details 的内容在关闭时是 display: none 的,一个不存在的元素做不了 transition。(这里原本是很多人写 JS 测量的方案,先跳过。)

Chrome 131 开始,details 内部的内容有了一个可以选择的伪元素:

details::details-content {
  /* 所有非 summary 的内容都在这里面 */
}

这个伪元素的存在本身就很关键。它把”summary 之外的兄弟节点”包成了一个盒,让它们可以单独被选中和设置样式。

一个基础动画

details::details-content {
  block-size: 0;
  overflow: hidden;
  transition:
    block-size .3s ease,
    content-visibility .3s allow-discrete;
}

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

三行就够。原理是这样。

关闭状态下,内容盒的高度是 0,超出部分裁掉。展开时高度过渡到 auto。这一步需要浏览器支持对 auto 做过渡——也就是 interpolate-size: allow-keywords,这在根元素上设一次就行。

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

另一行 content-visibility .3s allow-discrete 是关键。原因在于 details 关闭时,浏览器会给内容加 content-visibility: hidden,让它在渲染树里消失。content-visibility 是一个离散属性,它的变化默认不能过渡——要么是 hidden,要么是 visible,没有中间状态。

allow-discrete 这个关键字改变了这一点。它告诉浏览器:这个属性虽然离散,但允许它和其他可过渡的属性一起,在过渡开始和结束的瞬间同步切换。看起来像是一堆奇怪的关键字串起来,但它确实让整个过渡变得连贯。

这里有个顺序细节。content-visibility 必须排在其他属性后面(或者在同一条 transition 里也生效),因为它是”开关”,决定内容在过渡的哪一端才真正出现。如果顺序写反,会出现内容瞬间出现然后又动画变大的诡异效果。

三、案例一:可搜索的 FAQ 手风琴

把两块能力综合起来,写一个完整的 FAQ 组件。加了 hidden="until-found" 之后,浏览器在页内搜索(Ctrl+F)的时候会自动展开包含匹配内容的面板,这个能力和 FAQ 场景配合得非常好。

<section class="faq">
  <details name="faq">
    <summary>如何申请退款?</summary>
    <div class="faq-body">
      <p>在订单详情页点击"申请退款",填写原因后提交。</p>
      <p>审核通过后,款项会在 3-5 个工作日原路退回。</p>
    </div>
  </details>

  <details name="faq">
    <summary>发票怎么开?</summary>
    <div class="faq-body">
      <p>付款完成后,在"我的订单"里点"申请开票"。</p>
    </div>
  </details>

  <details name="faq">
    <summary>账号可以在几台设备上登录?</summary>
    <div class="faq-body">
      <p>同时最多登录三台设备。</p>
    </div>
  </details>
</section>
:root {
  interpolate-size: allow-keywords;
}

.faq details::details-content {
  block-size: 0;
  overflow: hidden;
  transition:
    block-size .35s cubic-bezier(.4, 0, .2, 1),
    content-visibility .35s allow-discrete;
}

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

.faq summary {
  cursor: pointer;
  padding: 16px 0;
  font-weight: 600;
  list-style: none;
}

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

.faq summary::after {
  content: "+";
  float: right;
  transition: transform .25s;
}

.faq details[open] summary::after {
  content: "−";
}

这段 CSS 里有两个地方值得说。

为什么需要 list-style: none

很多浏览器会在 summary 前面渲染一个小三角作为展开状态指示器。这个三角的样式不太好统一控制,所以我习惯把它去掉,用 ::after 自己画一个。Safari 里需要用 ::-webkit-details-marker 伪元素来去掉,Chrome 和 Firefox 用 list-style: none 就够了。两条都写是最保险的。

用小三角还是加减号

这是一个设计层面的问题。+/− 的好处是太直观了,不会有人误解它的含义;三角形的方向变化更微妙,但在中文语境里更接近传统折叠面板的视觉。两个都可以,取决于项目已有的设计语言。

这里我用了 +/−,因为它和”展开/收起”在语义上绑定得更紧——加号是”再来一点”,减号是”收回去”。

关于 hidden=”until-found”

如果要让页内搜索能找到关闭状态的内容,把 details 的打开逻辑用 JavaScript 判断一下,加一个 hidden="until-found" 在更里层的内容上。这个特性主要是解决”用户按 Ctrl+F 找一个内容,但是内容在关闭的面板里,搜索不到”的痛点。

坦白说这个特性在浏览器里的实际体验参差不齐。Chrome 上工作得比较好,会滚动到位置并且自动打开包含它的 details。其他浏览器里还不太一致。用它之前最好在目标用户常用的浏览器里实测一遍。

四、案例二:侧栏筛选面板

电商后台里,侧栏筛选往往是几组折叠面板:价格区间、品牌、评分、库存状态。用户调整筛选条件的时候,希望一次只展开一组,不用滚动页面。

<aside class="filters">
  <details name="filter" open>
    <summary>价格区间</summary>
    <div class="filter-body">
      <label><input type="checkbox"> 0 - 99</label>
      <label><input type="checkbox"> 100 - 299</label>
      <label><input type="checkbox"> 300 - 999</label>
      <label><input type="checkbox"> 1000 以上</label>
    </div>
  </details>

  <details name="filter">
    <summary>品牌</summary>
    <div class="filter-body">
      <label><input type="checkbox"> 品牌 A</label>
      <label><input type="checkbox"> 品牌 B</label>
    </div>
  </details>
</aside>

这段没什么新东西,就是 name 加 open 而已。第一组的 open 属性让它在初始状态就展开。后续交互由浏览器接管。

有一个细节需要处理:details 的默认样式在容器里会自带一点 padding 和 margin,不同浏览器还不一样。可以在项目入口做一次 reset:

details {
  padding: 0;
  margin: 0;
}

details > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

把 summary 改成 flex 之后,那个用 ::after 画出来的加减号就能用 justify-content: space-between 顶到最右边,不用担心 float 布局在各种尺寸下的位置偏移。

五、案例三:移动端主导航

再写一个稍微不典型的用法。”汉堡菜单”这种东西以前都是给定制一个按钮,点击之后切换一个 class,然后菜单从侧边滑出来。details 也能做,而且因为有了 ::details-content,可以做出跟原来差不多的动画。

<header class="mobile-header">
  <details class="nav-toggle">
    <summary aria-label="打开菜单">
      <svg viewBox="0 0 24 24" width="24" height="24">
        <path d="M3 6h18M3 12h18M3 18h18" stroke="currentColor" stroke-width="2" fill="none"/>
      </svg>
    </summary>

    <nav class="nav-menu">
      <a href="/" rel="external nofollow" >首页</a>
      <a href="/products" rel="external nofollow" >产品</a>
      <a href="/pricing" rel="external nofollow" >定价</a>
      <a href="/about" rel="external nofollow" >关于</a>
    </nav>
  </details>
</header>
:root {
  interpolate-size: allow-keywords;
}

.nav-toggle::details-content {
  block-size: 0;
  overflow: hidden;
  transition:
    block-size .3s ease,
    content-visibility .3s allow-discrete;
}

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

.nav-menu {
  display: flex;
  flex-direction: column;
}

.nav-menu a {
  padding: 12px 16px;
  text-decoration: none;
}

这个结构和之前的两个案例几乎一样。区别在于被折叠的内容是一个 <nav> 元素,里面是一组链接。

但和”真正的汉堡菜单”比起来,它有几个差别。真正的汉堡菜单通常是覆盖在内容上方的(position: fixed),而 details 展开时会占据文档流空间,把下面的内容往下推。

如果设计稿要求覆盖式展开,需要在 ::details-content 里加一层定位:

.nav-toggle {
  position: relative;
}

.nav-toggle::details-content {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: white;
  box-shadow: 0 8px 24px rgba(0,0,0,.08);
  /* ... 其他过渡属性 */
}

此时内容脱离了文档流,不再撑开父元素。但高度动画仍然要应用在 block-size 上,和前面的写法保持一致。

这种”用 details 做下拉菜单”的写法我觉得比手动控制 display 要省事,因为键盘操作、屏幕阅读器语义都是原生支持的。唯一要注意的是:details 展开之后,点击内部链接不会自动关闭它。所以需要在 nav 里加一小段 JS 来关闭,或者接受”点击后页面跳转,details 状态随页面一起重置”的行为——后者在 SPA 里可能不成立,得根据路由方式判断。

六、五个实际踩过的坑

1. summary 必须直接是 details 的子元素

<!-- 无效:summary 被包在 div 里 -->
<details>
  <div class="wrapper">
    <summary>标题</summary>
  </div>
  <p>内容</p>
</details>

这种情况下浏览器找不到 summary,会退化成一个默认的”Details”文字作为展开按钮,或者干脆不响应点击。这是一个很容易犯的错误,因为很多人习惯把要样式化的东西都用 div 包一层。写 details 的时候要记住这一点。

2. ::details-content 里的部分属性会被忽略

这个伪元素的位置比较特殊,它介于 details 和内容之间。写样式的时候要意识到一点:它不是内容的替代,只是一层包裹盒。padding、margin 这些属性应用上去,可能和直接应用在内层 div 上的效果不完全一样。最稳妥的做法是用内层元素放具体的 padding 和背景,把 ::details-content 留给高度过渡和裁切。

3. details 展开时自动滚动

如果 details 靠近页面底部,展开之后内容会伸到视口外面。浏览器有时会自动滚动让它完全可见,有时不会。

如果希望展开时自动把 summary 保持在一个稳定位置,可以在 toggle 事件里做一点点处理:

document.querySelectorAll('details').forEach((el) => {
  el.addEventListener('toggle', () => {
    if (el.open) {
      el.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
    }
  });
});

这段代码只在需要的时候用。不加也能跑,只是在某些位置上体验不太好。

4. name 冲突导致意外互斥

前面提过,同名的 details 不需要在同一个父元素里。这意味着如果一个页面里有两块不同的手风琴,比如主导航的”更多”面板和侧栏的筛选面板,如果它们的 name 恰好相同,就会互相干扰。

命名规范上,建议每个功能区用一个唯一前缀,比如 nav-more、filter-price 这种。或者干脆不用 name 属性对全体生效的写法,而是配合容器查询或者动态设置 name 来做隔离。不过这属于工程习惯,需要团队内部约定。

5. details 的 open 属性和 CSS :has() 的配合

有一个很常见的需求:details 展开时,给 summary 改一下背景色或者加个边框。用 [open] summary 就够了。但如果你想改的是 details 外面某个元素,比如旁边的一条分隔线,就得用 :has():

.faq-section:has(details[open]) .separator {
  opacity: 0;
}

:has() 的性能开销会比普通选择器大一些,因为浏览器需要判断”这个父元素的后代里有没有匹配条件的元素”。如果页面里的 details 数量不多(比如十几个),影响可以忽略;如果页面里放了上百个,就要小心。

实测经验是::has( 里简单选择器(details[open])比复合选择器(details[open] > summary)要快得多。尽量让条件简单。

七、什么时候用 details,什么时候用别的

给一个粗略的判断原则。

如果需求是”展开显示一段内容,内容不需要复杂的交互”,用 details。这是它最擅长的地方——FAQ、帮助文档、代码示例、设置项的详细说明,都是它的主场。

如果需要”一次只能展开一个”,加上 name。不要为此写 JS。

如果内容很长、需要配合搜索或者目录,考虑 details 之外还得做个侧边目录。避免把所有内容都藏进 details 里,用户完全看不到页面有多少内容。

如果要做”覆盖在页面上的浮层”,不要用 details,那应该是 dialog 或者 popover 的领域。用 details 硬做浮层会在移动端和键盘操作上遇到一堆麻烦。

如果多个面板之间的状态需要联动(比如”选中的筛选条件”要实时反映到其他面板上),还是老实写 JS。details 没有提供任何”读取当前展开状态并同步给别的元素”的能力,这种场景下它只在动画和无障碍上减轻一点点负担。

八、写在最后

回过头看,从”用 JS 处理手风琴逻辑”到”用 <details name>“,真正减少的不只是那二十几行代码。影响更大的地方在于:原生行为不会像 JS 那样在边缘情况下出错。

浏览器对 details 的键盘操作、屏幕阅读器识别、打印样式、焦点管理都有一整套既定的处理。自己写 JS 的时候,这些细节你不容易全想清楚,反而是不出问题的时候感受不到它存在,一出问题就得查很久。

也许有人会问:name 属性看起来是个挺小的语法增补,为什么值得专门写一篇文章。

因为手风琴这个交互模式在网页上出现了二十年,前十九年大家都得写 JS 才能实现”互斥展开”,这个体验几乎是所有后台系统和内容网站的标配。现在变成了一行属性,用的人反而不多——不是因为它不好用,而是因为它太安静了,安静到大家不太会注意到它已经存在。

所以这篇文章的重点其实不在”教新东西”,而在提醒:那些看起来需要写 JS 的需求,很可能 HTML 早就提供了原生方案。每次遇到手风琴、折叠面板、展开详情,先搜一下这个元素最近有没有新能力,比直接上手写 JS 划算得多。

details 元素现代实战:name 手风琴与 ::details-content 展开动画
收藏 (0) 打赏

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

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

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

淘吗网 html details 元素现代实战:name 手风琴与 ::details-content 展开动画 https://www.taomawang.com/web/html/2824.html

常见问题

相关文章

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

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