原生之力:用 HTML `<details>` 元素零成本实现折叠面板、悬浮菜单与手风琴
摘要:还在用 <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> 来做吗?”答案往往是可以。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)