CSS 实现树状结构目录效果是这样的
纯 CSS 实现带连接线的树形组件 - 图1
整个实现没有用到任何 JavaScript,非常巧妙。
不过有时候还需要那种带连接线的样式,这样看起来层级会更清晰,就像这样
纯 CSS 实现带连接线的树形组件 - 图2
这是如何实现的呢?一起来看看吧~

一、details 和 summary

简单回顾一下,整体结构需要利用到 detailssummary,天然地支持内容展开和收起。这里有一个 MDN 的例子

  1. <details>
  2. <summary>System Requirements</summary>
  3. <p>Requires a computer running an operating system. The computer
  4. must have some memory and ideally some kind of long-term storage.
  5. An input device as well as some form of output device is
  6. recommended.</p>
  7. </details>

直接就实现了展开和收起
纯 CSS 实现带连接线的树形组件 - 图3
还可以支持多层嵌套,只需要将details当做展开的内容就行了,如下

  1. <details>
  2. <summary>项目1</summary>
  3. <details>
  4. <summary>文件夹0</summary>
  5. </details>
  6. <details>
  7. <summary>文件夹1-1</summary>
  8. <details>
  9. <summary>文件夹1-1-2</summary>
  10. </details>
  11. <details>
  12. <summary>文件夹1-1-3</summary>
  13. ...
  14. </details>
  15. </details>

这样就得到了一个简单的树状结构
纯 CSS 实现带连接线的树形组件 - 图4
看着还不像?那是因为现在还没有缩进,可以这样

  1. details{
  2. padding-left: 10px
  3. }

简单调整一下间距后得到这样的效果,是不是要清晰很多?
DM_20230705094244_001.PNG

二、绘制加号和减号

首先,默认的黑色三角太丑了,需要去掉。现代浏览器中,这个“黑色三角”其实是 ::marker生成的,而这个 ::marker是通过list-style生成,所以要去除就很简单了
旧版本浏览器需要通过专门的伪元素修改,::-webkit-details-marker::-moz-list-bullet ,现在都统一成了list-style

  1. summary{
  2. list-style: none;
  3. }

当然,也可以改变summarydisplay属性(默认是list-item)

  1. summary{
  2. display: flex;
  3. }

这样,默认的三角就去除了
DM_20230705094244_002.PNG
然后,绘制加号(➕)和减号(➖),由于还有外围一个正方形边框,可以用伪元素来绘制(当然,这是在可以使用的情况下),好处是可以直接用border画边框,这比用渐变方便的多,然后加号就是两段线性渐变,如下
DM_20230705094244_003.PNG
用代码实现就是

  1. summary::before{
  2. content: '';
  3. width: 14px;
  4. height: 14px;
  5. flex-shrink: 0;
  6. margin-right: 8px;
  7. border: 1px solid #999;
  8. background: linear-gradient(#999, #999) 50%/1px 10px no-repeat,linear-gradient(#999, #999) 50%/10px 1px no-repeat;
  9. }

调整一下间距,效果如下
DM_20230705094244_004.PNG
现在都是加号(➕),看不出哪些是展开的,所以还需要绘制减号(➖),可以用[open]属性来判断,相较于加号(➕)而言,只需要一个线性渐变就行了,实现如下

  1. details[open]>summary::before{
  2. background: linear-gradient(#999, #999) 50%/10px 1px no-repeat;
  3. }

现在就可以区分哪些是展开,哪些是折叠的了
DM_20230705094244_005.PNG
到了这一步,其实还有一个小问题,有些是不能继续展开的,因为已经到了最底层,没有内容了,所以希望在没有展开内容的时候不显示加号(➕)或者减号(➖),这应该如何判断呢?
其实很简单,在没有展开内容的情况下,其实只有summary单个标签,就像这种结构

  1. <details>
  2. <summary>文件</summary>
  3. <!--没有内容了-->
  4. </details>

提到单个标签,可以想到:only-child伪类,所以可以这样重置一下

  1. summary:only-child::before{
  2. display: none
  3. }

还有另外一种做法,那就是借助:not伪类,直接在前面的选择器上加一层判断

  1. summary:not(:only-child)::before{
  2. /*排除单个summary的情况*/
  3. }

这样会更加优雅~效果如下
DM_20230705094244_006.PNG
这样就能轻易的看出哪些是不能展开的了

三、绘制连接线

最后就是绘制连接线,也是 CSS 最灵活的、最有趣的一部分。
先从绘制实线开始,这样比较容易。
直接绘制可能有些难度,可以分解开来,一部分是垂直的,指向树的每个标题部分,所以直接绘制在summary上,还有一部分是竖直的,并且竖直部分会包含整个展开部分,因此可以把线条绘制在details上,用代码实现如下(为了区分,下面把垂直部分用红色表示)

  1. summary{
  2. /*水平线*/
  3. background: linear-gradient(#999,#999) 0px 50%/20px 1px no-repeat;
  4. }
  5. details{
  6. /*垂直线*/
  7. background: linear-gradient(#999, #999) 40px 0px/1px 100% no-repeat;
  8. }

效果如下
DM_20230705094244_007.PNG
看着好像有些凌乱?确实有很多线是多余的,比如树的最后一个节点,垂直线段不应该继续向下延伸了,最左侧的线也是多余的,下面是示意图,其实想要右边那样的效果
DM_20230705094244_new.PNG
首先是最左侧的线段,其实就是最外层,也就是第一层,要去除很简单,直接选中第一层的details以及下面的summary就行了,这里可以用子选择器>来实现

  1. .tree>details,
  2. .tree>details>summary{
  3. /*去除最外层的连接线*/
  4. background: none
  5. }

效果如下
DM_20230705094244_009.PNG
然后就是每层的最后一个子节点,如何将垂直线段去除呢?其实可以从HTML结构上入手,最后一层,其实就是最后一个details,所以将最后一个的背景尺寸改为刚好和垂直线段吻合

  1. details:last-child{
  2. background-size: 1px 23px;
  3. }

为了区分,下面将这一部分用蓝色表示
DM_20230705094244_010.PNG
还有一个小优化,现在最左侧第一层都是分开的,看着有些零散,这是因为前面这一步将所有最后一层的垂直线段都去掉了,所以需要还原这种情况,可以用子选择器>选到,如下

  1. .tree>details:not(:last-child)>details:last-child{
  2. background-size: 1px 100%;
  3. }

为了区分,下面将这一部分用紫色表示
DM_20230705094244_011.PNG
实线画出来了,虚线还远吗?
同样也可以用渐变实现,只不过需要用repeating-linear-gradient,因为虚线其实是不断重复的从实色到透明的渐变,示意如下
DM_20230705094244_012.PNG
用代码实现就是

  1. summary{
  2. /*水平虚线*/
  3. background: repeating-linear-gradient( 90deg, #999 0 1px,transparent 0px 2px) 0px 50%/20px 1px no-repeat;
  4. }
  5. details{
  6. /*垂直虚线*/
  7. background: repeating-linear-gradient( #999 0 1px,transparent 0px 2px) 40px 0px/1px 100% no-repeat;
  8. }

这样就实现了开头效果了
纯 CSS 实现带连接线的树形组件 - 图17
下面是完整CSS代码

  1. .tree summary{
  2. outline: 0;
  3. padding-left: 30px;
  4. list-style: none;
  5. background: repeating-linear-gradient( 90deg, #999 0 1px,transparent 0px 2px) 0px 50%/20px 1px no-repeat;
  6. /* background: linear-gradient(#999,#999) 0px 50%/20px 1px no-repeat; */
  7. }
  8. .tree details:last-child{
  9. background-size: 1px 23px;
  10. }
  11. .tree>details:not(:last-child)>details:last-child{
  12. background-size: 1px 100%;
  13. }
  14. .tree details{
  15. padding-left: 40px;
  16. background: repeating-linear-gradient( #999 0 1px,transparent 0px 2px) 40px 0px/1px 100% no-repeat;
  17. /* background: linear-gradient(#999, #999) 40px 0px/1px 100% no-repeat; */
  18. }
  19. .tree>details{
  20. background: none;
  21. padding-left: 0;
  22. }
  23. .tree>details>summary{
  24. background: none
  25. }
  26. .tree summary{
  27. display: flex;
  28. align-items: center;
  29. height: 46px;
  30. font-size: 15px;
  31. line-height: 22px;
  32. color: rgba(0, 0, 0, 0.85);
  33. cursor: default;
  34. }
  35. .tree summary::after{
  36. content: '';
  37. position: absolute;
  38. left: 10px;
  39. right: 10px;
  40. height: 38px;
  41. background: #EEF2FF;
  42. border-radius: 8px;
  43. z-index: -1;
  44. opacity: 0;
  45. transition: .2s;
  46. }
  47. .tree summary:hover::after{
  48. opacity: 1;
  49. }
  50. .tree summary:not(:only-child)::before{
  51. content: '';
  52. width: 14px;
  53. height: 14px;
  54. flex-shrink: 0;
  55. margin-right: 8px;
  56. border: 1px solid #999;
  57. background: linear-gradient(#999, #999) 50%/1px 10px no-repeat,linear-gradient(#999, #999) 50%/10px 1px no-repeat;
  58. }
  59. .tree details[open]>summary::before{
  60. background: linear-gradient(#999, #999) 50%/10px 1px no-repeat;
  61. }

也可以查看以下任意链接:

点击查看【codepen】

四、总结一下

以上就是本文的全部内容了,可以看到全部由 CSS 绘制而成,没有用到任何图片,是不是很简单呢?下面总结一下实现要点

  1. details 和 summary 原生支持展开收起
  2. details 和 summary 支持多层嵌套,这样就得到了简易的树状结构
  3. 逐层缩进可以通过给 details 添加内边距实现
  4. summary 的黑色三角形是通过 list-style 生成的,可以更改 display 属性去除
  5. 利用伪元素可以轻易实现 border 边框,这比用渐变方便的多
  6. 加号其实是两段线性渐变叠加而成,减号一段渐变就够了
  7. 连接线可以分成两段,垂直线段绘制在 details 上,水平线段绘制在 summary 上
  8. 多余的线段可以通过 :last-child和子选择器>去除
  9. 虚线其实是不断重复的从实色到透明的渐变,可以用repeating-linear-gradient绘制

相比于现有的组件库,原生实现最大的好处就是灵活性,合理运用选择器,各式各样的设计都能轻易实现,组件库可兼顾不了这么多。另外,兼容性方面也非常不错,主流浏览器均支持,IE 上虽然不支持 details 和 summary,但是通过 polyfill 解决,总的来说非常实用的,大可以放心使用。

参考资料

details: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/details
summary: https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/summary
CSS tree with line (codepen.io): https://codepen.io/Fcant/pen/NWEvNpe
polyfill: https://github.com/javan/details-element-polyfill