用纯CSS实现选项卡切换,还能兼顾键盘操作和屏幕阅读器的体验,这在以前多少得整点“黑魔法”。现在不一样了,HTML自带的 <details> 元素加上CSS Grid布局,特别是子网格(Subgrid)这个特性,让这一切变得顺滑起来。不用JavaScript,代码结构清晰,而且天生就带一点可访问性支持,可以说是目前一个相当巧妙的解决方案。下面就把这套组合拳的完整搭建过程掰开揉碎了讲讲。
结构,靠details撑场子
先把HTML的骨架搭起来。需要一组 <details> 元素,外面套一个容器,比如就叫 .grid。每个 <details> 就是一个选项卡,里面必须有 <summary> 作为点击的标题区域,剩下的部分就是点击后展开的内容面板。
<div class="grid">
<details class="item" name="tabGroup" open>
<summary>选项一</summary>
<div>这里是第一个选项卡的内容,打开状态默认展示。</div>
</details>
<details class="item" name="tabGroup">
<summary>选项二</summary>
<div>点击第二个标题,第一个面板会收起,这个面板就展开了。</div>
</details>
<details class="item" name="tabGroup">
<summary>选项三</summary>
<div>第三个面板的内容,同样支持键盘操作。</div>
</details>
</div>这里有个关键点:给每个 <details> 设置相同的 name 属性值,比如这里都叫 "tabGroup"。这个操作就像给它们拉了个群,让它们在群里互斥——点开一个,同名的其他 <details> 会自动关上,这就模拟出了选项卡“同时只能开一个”的核心效果。open 属性标记了默认打开哪一个。整个结构不需要额外的 <div> 去模拟选项卡的标题栏,HTML语义上就是原生的折叠面板,这为后续的可访问性打下了基础。
布局,Grid打底Subgrid精装
CSS部分要干两件大事:一是让所有选项卡的内容面板共用一个展示区域,二是把 <summary> 标题栏排成一行,看起来像真的选项卡标签。
先给外层容器 .grid 设置网格。需要两行,第一行用来放标题栏,高度根据内容自动撑开;第二行用来放内容面板,占满剩余空间。列数就按选项卡数量来,这里三个选项卡,就分成三列。
.grid {
display: grid;
grid-template-rows: auto 1fr;
grid-template-columns: repeat(3, 1fr);
gap: 0 1rem;
}接下来是关键,让每个 <details> 元素变成子网格(subgrid),继承父容器的行列结构。这样,每个 <details> 内部的 <summary> 和内容区域就能直接对齐到 .grid 的行列线上,不用再手动算尺寸。
.item {
display: grid;
grid-template-rows: subgrid;
grid-template-columns: subgrid;
grid-row: 1 / -1;
grid-column: 1 / -1;
}把 .item 的 grid-row 和 grid-column 都设成 1 / -1,是为了让每个选项卡都铺满整个 .grid 容器。这时候所有选项卡会完全重叠在一起,看着乱,但别急,这正是布局的基础。
内容区,统一位置只显示一个
所有选项卡面板内容应该都出现在第二行,并且横跨所有列。这里可以用 ::details-content 伪元素来精准控制内容区域的位置,避免在HTML里多包一层 <div>。
.item::details-content {
grid-row: 2;
grid-column: 1 / -1;
padding: 1rem;
border-top: 2px solid #e2e8f0;
}现在所有内容面板都挤在同一个位置了,但得让没打开的那些面板隐藏起来。利用 [open] 选择器,只显示当前打开的 <details> 的内容。
.item:not([open])::details-content {
display: none;
}这一步做完,界面清爽了,只会看到一个选项卡的内容。但标题栏还是堆叠在一起的,需要把它们按列分开。
标题栏,用变量分配合适的列
每个 <summary> 都应该位于网格的第一行,并且分别占据不同的列。可以用 :nth-of-type 选择器硬编码,但更好的办法是用CSS变量,这样以后增减选项卡数量时维护起来方便。
首先给每个 <summary> 统一设置 grid-row 为第一行,并设置一个变量 --n 来控制它占据哪一列。
summary {
grid-row: 1;
grid-column: var(--n) / span 1;
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.item[open] summary {
border-bottom-color: #3b82f6;
font-weight: 500;
}然后在HTML里,通过内联样式给每个 <details> 设置不同的 --n 值。第一个选项卡的 --n 是1,第二个是2,第三个是3,以此类推。
<details class="item" name="tabGroup" open style="--n: 1">
<summary>选项一</summary>
<div>内容一</div>
</details>
<details class="item" name="tabGroup" style="--n: 2">
<summary>选项二</summary>
<div>内容二</div>
</details>
<details class="item" name="tabGroup" style="--n: 3">
<summary>选项三</summary>
<div>内容三</div>
</details>这样一来,三个 <summary> 就分别坐到了网格的第一行第一列、第一行第二列、第一行第三列的位置,视觉上就成了并列的选项卡标签。
叠放顺序,让可点击区域露出来
因为每个 <details> 都铺满了整个 .grid,它们的 <summary> 虽然位置分开了,但元素本身的块级背景还是会相互覆盖,导致只能点到最后一个 <summary>。解决办法是给 <summary> 加上一个 z-index,让它们浮在顶层。
summary {
position: relative;
z-index: 1;
}这一步做完,所有选项卡标签都能正常点击了,点哪个标签,对应的内容面板就显示出来,同时其他面板自动隐藏。
| 步骤 | 操作要点 | 作用 |
|---|---|---|
| 1 | 给details加相同name属性 | 实现互斥打开 |
| 2 | 外层grid定义两行三列 | 划分标题和内容区域 |
| 3 | details启用subgrid | 内部元素对齐父级网格线 |
| 4 | 用::details-content固定内容区 | 统一面板位置并控制显示 |
| 5 | summary通过变量分配列位置 | 标签栏自动排开 |
| 6 | summary加z-index | 保证点击区域不被遮挡 |
如果手头项目里选项卡数量不固定,比如后台系统里不同模块的选项卡个数不一样,手动写内联样式变量确实有点呆。可以借助模板引擎来批量生成,比如在Liquid、Nunjucks或者PHP里循环输出,把循环的索引值赋值给 --n 变量。这样不管选项卡是三个还是五个,代码结构都是一致的,维护起来也省心。
这套方案用 <details> 自带的状态管理代替了JavaScript的点击切换逻辑,用CSS Grid的Subgrid特性解决了复杂的对齐问题。::details-content 虽然目前还不是所有浏览器都完全支持,但对于现代Web开发来说,已经是一个非常干净且值得尝试的选项卡实现方式。
