摘要:还在用 <div> 配合复杂的 JavaScript 状态管理来写折叠面板吗?HTML 原生提供的 <details> 元素,不仅能零代码实现展开/收起,配合 name 属性更能轻松构建互斥手风琴。本文将带你全面认识这一被低估的原生组件,并通过三个实战案例,彻底掌握它的使用技巧。

一、认识 <details>:不只是个标签

1.1 它是什么

在这里插入图片描述

<details> 是一个 HTML 元素,由开始标签 <details>、结束标签 </details> 以及它们之间的内容共同构成。它代表一个“披露组件”,用户可以通过点击来展开或隐藏其中的详细信息。

一个完整的 <details> 元素通常包含两个部分:

  • <summary> 元素:作为始终可见的摘要或标题,用户点击它来切换内容的可见性。
  • 其他流内容:被折叠起来的部分,可以是段落、列表、图片等任意 HTML 内容。
<details>
  <summary>点击这里展开</summary>
  <p>我是被隐藏的详细内容,现在你能看到我了。</p>
</details>

1.2 核心特性一览

在这里插入图片描述

特性 说明
零 JS 交互 浏览器原生支持点击 <summary> 展开/收起内容,无需任何脚本。
open 属性 布尔属性控制内容的可见状态。存在时内容显示,不存在时内容隐藏。
name 属性 给多个 <details> 设置相同的 name 值,它们会在同一组内形成互斥

二、样式定制:告别默认样式

浏览器默认会给 <details> 元素添加一个三角形 ::marker 图标。在实际项目中,我们通常需要替换它。

清除默认标记

summary {
  list-style: none;   /* 移除默认的三角形标记 */
}

自定义展开/收起图标:可以在 <summary> 内使用 ::after 伪元素添加自定义箭头,再配合 details[open] 选择器来区分展开与收起状态:

summary::after {
  content: "▼";
  transition: transform 0.2s;
  /* 收起时的样式 */
}

details[open] summary::after {
  transform: rotate(180deg);
  /* 展开时旋转 180 度,变为 ▲ */
}

details[open] 是一个关键选择器,它让你能根据展开/收起状态改变任何子元素的样式。

三、实战案例:三种经典交互模式

下面通过三个实际场景,展示 <details> 元素在不同需求下的应用方式。每个案例都附有核心代码和原理解析。

案例一:自定义按钮的文章展开效果

在这里插入图片描述

场景:文章阅读页面,需要在摘要下方提供“阅读全文”按钮,点击后展开正文,同时显示“收起内容”按钮。

核心思路:利用 <summary> 展示摘要和“阅读全文”按钮,通过 JavaScript 精准控制 open 属性的切换,同时阻止 <summary> 原生点击行为的干扰。

HTML 结构

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8">
    <title>展开更多</title>
    <link rel="stylesheet" href="./css/style.css">
  </head>

  <body>
    <article class="card">
      <header>
        <div class="meta">
          <b class="tag">设计思维</b>
          <div class="reading-time">阅读时间:<span>5</span>分钟</div>
        </div>
        <h1>留白与呼吸:现代网页设计中的负空间艺术</h1>
        <address class="author-info">
          <img src="./img/avatar.jpg" alt="作者头像" class="author-avatar">
          <div class="author-details">
            <span class="author-name">林清风</span>
            <time class="publish-time" datetime="2026-01-12">2026 年 1 月 12 日</time>
          </div>
        </address>
      </header>

      <hr>

      <details class="article-details">
        <summary>
          <p class="lead">在信息过载的数字时代,优秀的设计往往不在于添加多少元素,而在于懂得何时不做任何事。留白——这个被误解为"空白浪费"的设计要素,实则是引导视线、建立层次、创造优雅体验的关键力量。</p>
          <span class="expand-btn">阅读全文</span>
        </summary>
        <section class="content">
          <p>当我们浏览一个网页时,眼睛会本能地寻找休息的地方。这些休息点,就是设计师精心布置的留白。它们不是设计的缺席,而是设计本身——是内容之间的呼吸空间,是视觉节奏的休止符。</p>
          <h2>留白的科学</h2>
          <p>
          研究表明,适当的留白可以<strong>提高20%的阅读理解率</strong>。当文字周围有足够的空间时,读者的眼睛可以更轻松地追踪行距,大脑也能更好地处理信息。这就是为什么高端杂志总是看起来比廉价传单更易读——不是因为字体更贵,而是因为<em>空间更奢侈</em></p>
        <h2>层次与节奏</h2>
        <p>留白创造了视觉层次。通过调整元素之间的距离,我们可以暗示它们之间的关系:相近的元素被视为一组,远离的元素则形成区隔。这种<strong>"邻近性原则"</strong>是格式塔心理学的核心,也是所有排版设计的基础。
        </p>
        <p>在网页设计中,我们使用不同权重的留白来建立节奏。大段之间的空白(宏观留白)让内容模块化,行距和字间距(微观留白)则确保阅读的舒适性。一个经验丰富的设计师会像作曲家安排音符一样安排这些空间。</p>
        <h2>少即是多的美学</h2>
        <p>苹果公司的设计哲学深刻影响了整个行业。他们的产品页面往往是大面积的留白配合精炼的文案和一张产品图。这种<em>少即是多</em>的美学不是偷懒,而是对自信的表达——当品牌足够强大时,它不需要用噪音来证明自己的存在。
        </p>
        <p>然而,留白的使用需要克制和平衡。过多的留白会让页面显得空洞,过少则会造成压迫感。找到那个平衡点,是设计师经验与直觉的体现。</p>
        </section>
        <span class="collapse-btn">收起内容</span>
      </details>
    </article>

    <script src="./js/main.js"></script>
  </body>
</html>

JavaScript 逻辑

const details = document.querySelector(".article-details");
const btnExpand = details.querySelector('.expand-btn');
const btnCollapse = details.querySelector('.collapse-btn');
const summary = details.querySelector('summary');

// 阻止点击 summary 展开/收起
summary.addEventListener('click', e => {
  if (e.target.tagName.toLowerCase() === 'button') return;
  e.preventDefault();
});

// 点击展开按钮
btnExpand.addEventListener("click", function() {
  details.open = true
})

// 点击收起按钮
btnCollapse.addEventListener('click', () => {
  details.open = false;
});

CSS 样式

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  padding: 60px 20px;
  background-color: #f7f8fa;
  font-size: 14px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  color: #2c3e50;
}

.card {
  width: 880px;
  margin: 0 auto;
  padding: 32px 40px 20px;
  background-color: #fff;
  border-radius: 16px;
  box-shadow: 0 1px 3px rgb(0 0 0 / .04), 0 8px 24px rgb(0 0 0 / .06);
  transition: box-shadow .3s;
}

.card:hover {
  box-shadow: 0 2px 8px rgb(0 0 0 / .06), 0 12px 32px rgb(0 0 0 / .1);
}

.meta {
  display: flex;
  align-items: center;
  gap: 16px;
}

.meta > .tag {
  color: #e74c3c;
}

.meta > .reading-time {
  color: #7f8c8d;
}

.card h1 {
  margin: 20px 0 20px;
  font-size: 28px;
  color: #1a1a1a;
}

.author-info {
  display: flex;
  gap: 12px;
  font-style: normal;
}

.author-info > .author-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
}

.author-info > .author-details {
  display: flex;
  flex-direction: column;
}

.author-details > .author-name {
  font-weight: 600;
}

.author-details > .publish-time {
  font-size: 13px;
  color: #95a5a6;
}

hr {
  height: 1px;
  margin: 20px 0;
  border: none;
  background: linear-gradient(to right, transparent, #e0e0e0, transparent);
}

.article-details > summary::marker {
  font-size: 0;
}

.article-details .lead {
  padding-left: 20px;
  border-left: 3px solid #e0e0e0;
  line-height: 1.8;
  color: #5a6c7d;
  transition: border-color 0.3s;
}

.article-details .lead:hover {
  border-left-color: #3498db;
}

.article-details .expand-btn,
.article-details .collapse-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 10px 0;
  padding: 8px 12px;
  background-color: transparent;
  font-weight: 600;
  color: #7f8c8d;
  cursor: pointer;
}

.article-details .expand-btn:hover,
.article-details .collapse-btn:hover {
  border-radius: 20px;
}

.article-details .expand-btn::before,
.article-details .collapse-btn::before {
  content: "";
  width: 16px;
  height: 16px;
  background-size: contain;
  background-repeat: no-repeat;
}

.article-details .expand-btn::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%237f8c8d' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
}

.article-details .collapse-btn::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%237f8c8d' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M5 15l7-7 7 7'%3E%3C/path%3E%3C/svg%3E");
}

.article-details .expand-btn:hover {
  border-color: #d6eaf8;
  background: #ebf5fb;
  color: #3498db;
}

.article-details .collapse-btn:hover {
  border-color: #fadbd8;
  background: #fdf2f2;
  color: #e74c3c;
}

.article-details .expand-btn:hover::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%233498db' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E");
}

.article-details .collapse-btn:hover::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23e74c3c' stroke-width='2'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M5 15l7-7 7 7'%3E%3C/path%3E%3C/svg%3E");
}

.article-details[open] .expand-btn {
  display: none;
}

.article-details .content {
  margin-top: 20px;
}

.article-details .content p {
  line-height: 1.8;
  text-align: justify;
  color: #2c3e50;
}

.article-details .content strong {
  font-weight: 600;
  color: #1a1a1a;
}

.article-details .content em {
  font-style: italic;
  color: #5a6c7d;
}

.article-details .content h2 {
  margin: 10px 0;
  font-size: 20px;
  color: #1a1a1a;
}

::details-content {
  transition: height .2s ease, content-visibility .2s ease allow-discrete;
  height: 0;
  overflow: clip;
}

details {
  interpolate-size: allow-keywords;
}

[open]::details-content {
  height: auto;
}

要点解析

  • 由于“阅读全文”和“收起内容”两个自定义按钮都在 <summary> 内部或其兄弟位置,需要先阻止 <summary> 的默认点击行为,防止用户在点击按钮时触发原生的展开/收起。
  • 展开和收起完全由 JS 通过设置 details.open 来控制,逻辑清晰。
  • CSS 利用 [open] 选择器自动切换两个按钮的显示/隐藏,无需手动管理样式状态。

案例二:纯 HTML 实现的 FAQ 手风琴

在这里插入图片描述

场景:常见问题页面,同一时间只展开一个问题的答案。

核心思路:利用 name 属性实现零 JavaScript 的互斥展开。

HTML 结构

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <title>手风琴效果</title>
  <link rel="stylesheet" href="./css/style.css">
</head>

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

    <details name="faq-group">
      <summary>什么是 HTML 的 details 标签?</summary>
      <div class="content">
        details 是 HTML5 提供的原生折叠组件,可以用于展示/隐藏内容,无需额外 JS。
      </div>
    </details>

    <details name="faq-group">
      <summary>如何实现手风琴效果(互斥展开)?</summary>
      <div class="content">
        使用 details 元素的 name 属性,相同 name 值的 details 会自动实现互斥。
      </div>
    </details>

    <details name="faq-group">
      <summary>这个组件适合用在哪些场景?</summary>
      <div class="content">
        常用于 FAQ、折叠菜单、说明文档等需要分块展示内容的场景。
      </div>
    </details>
  </div>
</body>

</html>

CSS 样式

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  padding: 40px;
  background-color: #f5f6f7;
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
  font-size: 14px;
  color: #222;
}

.faq {
  max-width: 800px;
  margin: 0 auto;
}

.faq > h2 {
  margin-bottom: 20px;
  text-align: center;
}

details {
  margin-bottom: 15px;
  padding: 12px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.05);
  interpolate-size: allow-keywords;
}

details > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-right: 20px;
  list-style: none;
  font-weight: 500;
  cursor: pointer;
}

details > summary::after {
  content: "";
  width: 16px;
  height: 16px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
  background-size: contain;
  transition: transform 0.2s;
}

details[open] summary::after {
  transform: translateY(-50%) rotate(180deg);
}

.content {
  margin-top: 10px;
  line-height: 1.5;
}

::details-content {
  transition: height 0.2s ease, content-visibility 0.2s ease allow-discrete;
  height: 0;
  overflow: clip;
}

[open]::details-content {
  height: auto;
}

要点解析

  • 这是所有案例中最简洁的一个——完全不需要 JavaScript
  • 只要给多个 <details> 元素设置相同的 name 属性值(此处为 "faq-group"),浏览器就自动接管互斥逻辑:打开一个,其他同组元素自动关闭。
  • 配合案例开头提到的 details[open] summary::after 箭头旋转技巧,可以做出流畅的展开动画效果。

案例三:点击触发的导航悬浮菜单

在这里插入图片描述

场景:网站顶部导航栏,点击“产品”、“解决方案”等菜单项时弹出下拉面板。

核心思路:结合 name 属性的互斥能力和外部点击关闭的 JS 辅助,实现接近原生控件体验的下拉菜单。

HTML 结构

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <title>悬浮菜单</title>
  <link rel="stylesheet" href="./css/style.css">
</head>

<body>
  <header class="site-header">
    <nav class="nav">
      <ul class="nav-list">
        <li class="nav-item">
          <a class="nav-link" href="#">首页</a>
        </li>

        <li class="nav-item">
          <details name="nav-menu">
            <summary class="nav-summary">产品</summary>
            <ul class="dropdown">
              <li><a href="#">产品概览</a></li>
              <li><a href="#">功能介绍</a></li>
              <li><a href="#">价格方案</a></li>
            </ul>
          </details>
        </li>

        <li class="nav-item">
          <details name="nav-menu">
            <summary class="nav-summary">解决方案</summary>
            <ul class="dropdown">
              <li><a href="#">企业服务</a></li>
              <li><a href="#">教育行业</a></li>
              <li><a href="#">电商场景</a></li>
            </ul>
          </details>
        </li>

        <li class="nav-item">
          <details name="nav-menu">
            <summary class="nav-summary">文档</summary>
            <ul class="dropdown">
              <li><a href="#">快速开始</a></li>
              <li><a href="#">API 文档</a></li>
              <li><a href="#">更新日志</a></li>
            </ul>
          </details>
        </li>

        <li class="nav-item nav-spacer">
          <a class="nav-link" href="#">登录</a>
        </li>
      </ul>
    </nav>
  </header>

  <script src="./js/main.js"></script>
</body>

</html>

CSS 样式

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

summary {
  list-style: none;
}

body {
  background-color: #f7f8fa;
  font-size: 14px;
  font-family: "Franklin Gothic Medium", "Arial Narrow", Arial, sans-serif;
  color: #222;
}

.site-header {
  background-color: #fff;
  border-bottom: 1px solid #e8e8e8;
}

.nav {
  width: 1100px;
  margin: 0 auto;
}

.nav-list {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 64px;
}

.nav-item {
  position: relative;
}

.nav-link,
.nav-summary {
  display: inline-flex;
  align-items: center;
  height: 40px;
  padding: 0 14px;
  text-decoration: none;
  line-height: 40px;
  color: inherit;
  border-radius: 10px;
  cursor: pointer;
}

.nav-link:hover,
.nav-summary:hover {
  background: #f2f3f5;
}

.nav-summary::after {
  content: "";
  width: 6px;
  height: 6px;
  margin-left: 8px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translateY(-1px);
  transition: transform 0.2s ease;
}

details[open] > .nav-summary {
  background-color: #f2f3f5;
}

details[open] > .nav-summary::after {
  transform: rotate(-135deg) translateY(-1px);
}

details {
  position: relative;
}

.dropdown {
  position: absolute;
  top: calc(100% + 10px);
  left: 0;
  min-width: 180px;
  padding: 8px;
  border: 1px solid #e8e8e8;
  background: #fff;
  list-style: none;
  border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
}

.dropdown li + li {
  margin-top: 4px;
}

.dropdown a {
  display: block;
  padding: 10px 12px;
  text-decoration: none;
  color: #222;
  border-radius: 8px;
  transition: background-color 0.2s ease;
}

.dropdown a:hover {
  background: #f5f6f8;
}

.nav-spacer {
  margin-left: auto;
}

JavaScript 逻辑

// 获取所有 details 元素
const menus = document.querySelectorAll('details[name="nav-menu"]');

// 点击页面任意位置关闭所有 details
document.addEventListener('click', (e) => {
  // 判断点击是否在 details 区域外
  menus.forEach(menu => {
    if (!menu.contains(e.target)) {
      menu.removeAttribute('open'); // 关闭
    }
  });
});

// 防止点击菜单内时,外部点击事件触发
menus.forEach(menu => {
  menu.addEventListener('click', (e) => {
    e.stopPropagation(); // 阻止事件冒泡,防止外部点击被触发
  });
});

要点解析

  • 使用 name="nav-menu" 保证多个菜单之间互斥——点击“产品”展开后,再去点击“解决方案”,前者的菜单会自动收起。
  • <details> 默认不会在点击外部区域时自动关闭,因此需要借助 JavaScript 监听全局点击事件,判断点击目标不在元素内部时,手动移除 open 属性。
  • 菜单内部的点击事件通过 stopPropagation() 阻止冒泡,避免触发全局关闭逻辑。

四、方案对比与选型建议

在这里插入图片描述

案例 是否需要 JS 关键技术点 适用场景
自定义按钮展开 需要(少量) 阻止 <summary> 默认行为 + 手动控制 open 阅读更多、长内容折叠
FAQ 手风琴 不需要 name 属性互斥 FAQ、设置页面、分组展示
导航悬浮菜单 需要(少量) name 互斥 + 外部点击关闭 导航下拉、操作菜单、筛选器

实践建议

  • 优先思考是否能用 <details> 替换手写的 <div> + 状态变量组合,尤其是简单折叠和互斥场景。
  • name 属性是目前实现手风琴最语义化、性能最优的方式,在兼容性允许的前提下应优先采用。
  • 对于需要“点击外部关闭”的场景(如下拉菜单),少量 JS 辅助是必要的,但整体代码量远少于完全自建方案。

五、写在最后

<details> 元素是 HTML5 标准中一个看似简单、实则有深度的组件。它用原生语义替代了多年来我们反复用 <div> 和 JavaScript 堆砌出来的“假折叠”,让代码更干净、体验更统一。

下次面对折叠面板的需求时,不妨先问自己一句:“这个,能用 <details> 来做吗?”答案往往是可以。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐