简介:纯CSS实现的可无限扩展族谱树形结构,专为Web前端、HTML5与PHP开发场景打造,可直观呈现家谱、组织架构、目录树等层次化数据,也能用于决策树与流程图的导航展示。压缩包内仅有1个HTML文件,体积仅2KB,无任何外部依赖,浏览器直接打开即可查看完整示例,适合作为轻量级前端模板快速套用。实现中综合运用display、position、float等布局属性控制节点排列,借助:before和:after伪类绘制父子节点连接线,并通过递归嵌套实现无限级扩展,同时保留:hover交互反馈,兼顾视觉效果与操作体验。代码结构清晰,可按需调整节点形状、颜色与字体,增加展开折叠功能,通过媒体查询适配各类屏幕,也可结合JavaScript懒加载优化海量节点的渲染性能,整体设计贴合实际项目需求。该资源已有1270人学习/下载,适合前端开发者作为CSS布局技巧的参考范例,也可用于企业信息架构梳理或作为前端课程中的实战案例。
1. 一个HTML页面搞定家族树:纯CSS方案比你想的更实用
给某个内部展示项目做家谱模块的时候,对方明确提了个要求:成员关系要一目了然,但不能依赖任何图表类框架,因为活动场地那台电脑可能连不上网。我第一反应是想用canvas手动画一棵族谱树,后来冷静下来一算,成员也就三四十个,交互只需要“看”和“找”,最合理的方式反而是纯CSS方案。一个index.html配一个tree.css,成员按嵌套列表写进HTML,父子连线全部由伪元素绘制,整页零JavaScript,拖进浏览器直接出效果。那次之后我把这套写法沉淀成了模板,后续做族谱、组织架构、部门层级都复用同一套思路。这篇文章就把这个模板从头拆一遍,从目录结构到每一行关键CSS,再到改数据、排查问题、做二次开发的完整流程。
2. 技术选型与目录结构:零依赖怎么组织项目
2.1 为什么放弃图表库:纯CSS树形的边界和优势
提到画家族树,多数人的默认答案是用图表库。库确实强,内置缩放、拖拽、大数据量渲染,按需引入也动不动几十KB起步。对一个静态家谱页面来说,体积只是次要问题,真正的代价在学习成本和定制成本:你得先学数据格式、初始化配置、样式覆盖钩子,真改起来会发现在库的框架里做“过继、早逝、配偶入谱”这些特殊标记,远不如直接写标签来得快。
纯CSS方案的适用边界很清楚:数据是静态的、层级深度不超过六层、节点数量在几十个以内。在这个范围内它的优势实打实摆着。第一,零依赖,不需要构建工具、不需要CDN,扔在局域网内任何机器上都能渲染;第二,所见即所得,打开index.html找到一个名字直接改,保存刷新立刻生效,全程不经过构建、不碰数据格式,这恰好是家谱维护里频率最高的操作。
当然它也有扛不住的时候,比如节点到了几百个、需要拖拽调整谱系关系、需要按年份筛选成员,那再上图表库或走数据驱动方案不迟。我判断标准就一条:数据动不动。静态用纯CSS,动态才上工具,这样既不委屈自己,也不过度设计。
2.2 目录结构与文件职责:两个文件撑起一个家谱
整个项目拆开看,核心文件只有两个,剩下的是说明文档。实际交付的包结构是这样:
family-tree/ ├── index.html # 家族树页面,全部成员数据写在这里 ├── css/ │ └── tree.css # 树形布局、连线、节点样式、响应式 └── README.md # 维护说明:增删成员、数据规范、排错记录我在很多静态项目里见过一种过度拆分:HTML里套模板语法,CSS拆成七八个分色文件,最后连自己都记不清哪个文件管哪一段。对族谱树形这个场景来说,两个文件是性价比最高的组织方式。index.html负责数据,tree.css负责呈现,职责单一清晰;将来要接模板引擎或者数据驱动,也只是把index.html里的<li>结构替换成渲染输出,两个核心文件的位置不用动。
里面对应关系也很直白:
| 文件 | 职责 | 维护者通常关心的内容 |
|---|---|---|
| index.html | 全部成员数据和HTML骨架 | 加一个人就在对应<li>节点下加一段标签 |
| css/tree.css | 布局、连线、视觉区分 | 调上下间距、线颜色、节点字号 |
| README.md | 维护规范与问题记录 | 新人接手时看这里 |
这套结构下工作流非常单调:改人打开index.html,改样式打开tree.css,不需要第三个入口。树形结构的核心逻辑全部集中在CSS里,接下来就进入重头戏,看嵌套列表怎么一步步长成一棵树。
3. 核心模块拆解:一层嵌套列表如何长成家族树
3.1 HTML结构:列表嵌套就是父子关系的骨架
先看一个四代同堂的例子:曾祖父膝下三个儿子,二房又生了两个孩子。对应的HTML是这样的:
<div class="family-tree"> <ul> <li> <div class="member"> 曾祖父 <span class="lifespan">1880—1952</span> </div> <ul> <li> <div class="member">长房</div> <ul> <li><div class="member">堂伯</div></li> <li><div class="member">堂叔</div></li> </ul> </li> <li> <div class="member">次房(本支)</div> <ul> <li><div class="member">父亲</div></li> <li><div class="member">我</div></li> </ul> </li> <li> <div class="member">三房</div> <ul> <li><div class="member">堂姑</div></li> </ul> </li> </ul> </li> </ul> </div>这段结构的关键点有三个。第一,外层ul是整个家族的根,里面只放一个li代表始祖,子树全部从它展开。第二,每个li内部用div.member放成员名牌,姓名和生卒年都能塞进去。第三,一个li内部如果再出现ul,就表示这位成员有后代,子孙按相同规则递归嵌套下去,层级关系和血缘关系一一对应。
这种写法的好处是“父子关系完全由缩进表达”,没有ID、没有parent字段、没有数据映射,维护者直接看嵌套层级就懂整个谱系。哪怕完全不懂编程的人,也能按“把某人一家放进某人的名牌下面”这种直觉完成维护,这种事情在手写族谱场景中非常关键。我习惯把生卒年用span.lifespan挂在姓名后面,这样整棵树扫一眼就能判断辈分年代,信息密度对族谱页面来说特别实用。
3.2 CSS连线逻辑:before、after伪元素各画半条线
HTML只提供了骨架,树形的形态全靠CSS撑起来。连线的基本思路并不复杂:每个li用padding-top留出垂直空间,用::before和::after两个伪元素画出水平线和垂直线,最后用ul::before补上父节点竖直向下的那一条短线。先看核心代码:
.family-tree ul { padding-top: 30px; /* 为子节点留出垂直间距 */ position: relative; /* 伪元素以ul为定位基准 */ } .family-tree li { display: inline-block; /* 兄弟节点水平并排 */ text-align: center; list-style: none; position: relative; padding: 30px 8px 0 8px; } /* 水平连接段:从节点顶部中心向左/右各画半个线段 */ .family-tree li::before, .family-tree li::after { content: ""; position: absolute; top: 0; height: 30px; width: 50%; border-top: 1px solid #b3a58a; } .family-tree li::before { right: 50%; /* 左半边:从左侧伸向中心 */ } .family-tree li::after { left: 50%; /* 右半边:从中心伸向右边缘 */ border-left: 1px solid #b3a58a; /* 这条左边框会形成竖直下坠短线 */ }这里需要停下来把伪元素的分工讲清楚。li::before定位在right: 50%,宽度是父级一半,因此它的盒子从节点左侧一直延伸到中心,border-top只在这条路径上画水平线,这就是左半段。li::after反过来,left: 50%让它从中心伸向右边缘,除了border-top还多一条border-left,这条左边框沿着盒子的左边缘向下延伸,正好在节点中心形成一条竖直短线,连接到成员名牌的顶部。所以每个普通节点上方是“一根半截横线加一根竖线”的组合。
还没完,子节点头顶的水平横线得有个来源接到父节点上。这个缺口由ul::before来补:
.family-tree ul ul::before { content: ""; position: absolute; top: 0; left: 50%; height: 30px; border-left: 1px solid #b3a58a; }这个选择器只命中嵌套在另一层ul里的ul,也就是所有非根级的子列表。它在子列表顶部正中间画一条30px的竖直短线,向下接到父节点底部,从而完成“父节点 → 竖向短线 → 水平横线 → 各子节点竖线”的完整链路。整棵树的连线完全由这一套规则递归生成,无论嵌套多少层,每一层都复用相同的逻辑。
代码里反复出现的30px和8px是必须统一维护的参数:padding-top和伪元素height要一致,否则竖线长度和节点间距会错位;8px是节点之间的水平间隔,节点越多这个值可以调得越小。我一般把它们抽出成CSS变量:
.family-tree { --v-gap: 30px; /* 垂直方向:父节点到子节点的间距 */ --h-gap: 8px; /* 水平方向:兄弟节点之间的间距 */ }后面所有用到的位置都改成var(--v-gap)和var(--h-gap),调样式只改这两个值。这个习惯看着不起眼,但在层级多的时候能省掉大量反复试错的力气。
3.3 分支与特殊标记:直系、旁系、配偶的区分写法
现实中的族谱不是二叉树,需要处理配偶、过继、早逝这些特殊情况。纯CSS方案的做法是给节点加状态类名,样式交给CSS处理,结构本身保持干净。常见的写法是这样:
<li> <div class="member">长房</div> <span class="spouse">配·某氏</span> </li> <li class="deceased"> <div class="member">夭折·次子</div> </li> <li class="adopted"> <div class="member">过继·某孙</div> </li>对应的CSS:
.family-tree .member { display: inline-block; padding: 6px 14px; border: 1px solid #b3a58a; border-radius: 4px; background: #faf6ef; line-height: 1.5; white-space: nowrap; } .family-tree .spouse { margin-left: 6px; color: #8c7b62; font-size: 0.85em; } .family-tree li.deceased .member { border-style: dashed; /* 早逝成员用虚线框 */ color: #7d7468; } .family-tree li.adopted .member { background: #f0ede6; /* 过继成员用底色区分 */ }这套做法的核心价值在于:所有分支语义都附加在类名上,不改变HTML嵌套结构。正常成员实线框,早逝成员虚线框,过继成员换底色,配偶通过旁注文本挂在成员名牌后面,树形连线完全不受影响。如果后面想把某些分支高亮,比如高亮“本支”五代直系,只需要给对应li加一个class="highlight",再写一段背景色就能实现,自由度比图表库高很多。
这也解释了为什么我不推荐在这种场景用canvas或SVG:一旦用图形绘制,每个节点都需要手动计算坐标、手动管理层级关系,修改一个人名都要重新走一遍数据更新逻辑;而嵌套列表加CSS的方式,节点坐标完全由浏览器自动计算,视觉和结构彻底解耦。接下来就到项目落地的关键环节——怎么跑起来、怎么改数据、踩了坑怎么排查。
4. 跑起来与避坑:从双击打开到可维护的完整流程
4.1 本地运行:零构建三步跑起来
这个项目不需要任何构建步骤,运行方式简单到不像一个“项目”。拿到资源后只需要保证两点:index.html和css/tree.css的相对路径正确,然后用任意浏览器打开index.html即可。
如果后面要接数据渲染或者做模板化改造,我更推荐起一个本地静态服务,避免浏览器对本地文件访问权限的限制。最常见的做法是用Python的内置模块,一行命令:
cd family-tree python -m http.server 8080浏览器访问http://localhost:8080就能看到整棵家族树。这里8080是端口号,可以任意换,只要不跟本机其他服务冲突;cd family-tree是进入项目根目录,务必确认当前目录下有index.html。如果页面空白,先看浏览器控制台有没有报错,再检查路径大小写,CSS引用写成tree.css还是tree.CSS,目录层级是否匹配,静态项目九成跑不起来的问题出在这里。
另外建议用Chrome或Edge这类现代浏览器打开,倒不是纯CSS有什么兼容性门槛,而是它们自带的开发者工具能直接选中节点查看盒子模型。调树形布局时,那个“布局面板”比任何调试手段都直观,能直接看到伪元素盒子落在了哪个位置。
4.2 数据维护:新增一族人时改哪里
维护族谱最高频的操作就是“加人”和“调整分支”。规则只有一句话:找到这位成员的父亲或祖父节点,在对应li内部追加一个ul,再往ul里塞li。比如要给“我”名下加两个孩子:
<li> <div class="member">我</div> <ul> <li><div class="member">长子</div></li> <li><div class="member">次女</div></li> </ul> </li>浏览器会自动把两个孩子渲染成“我”下方的新分支,父节点连一条竖线下来,横线平均分给两个孩子,全程不需要修改任何CSS。这正是嵌套列表方案优于数据驱动方案的地方:改的是结构化文本本身,不存在数据迁移和格式校验问题。
有一种边界情况要注意:当一个分支只有一个孩子时,:only-child选择器会自动隐藏左右两半横线,只保留从父节点垂直下来的竖线,视觉上成为一条直线。这种默认行为在绝大多数族谱场景下是正确的。如果确实需要强制显示成带横线的分支,比如为了和其他兄弟分支对齐,可以给那个li加一个class="force-branch",再写一小段覆盖规则,把:only-child的隐藏行为绕过去。这种小口子让静态方案保留了足够灵活性,不会在特殊结构上卡死。
4.3 常见问题排查:连线错位、伪元素失效、渲染被截断
这部分整理自实际调试记录,每条都是真实遇到过的坑,按“现象 → 原因 → 解决”的顺序说。
坑一:子节点全部纵向排列,树形塌成一列。
现象:页面正常打开,但兄弟节点一个接一个竖着排,根本没有横向展开。
原因:.family-tree li没有设置display属性,默认的list-item是块级盒子,多个兄弟节点自然纵向堆叠。树形布局的第一块基石就是让兄弟节点横排。
解决:给li设置display: inline-block,并在外容器上处理inline-block的空白间隙问题:
.family-tree { font-size: 0; /* 消除inline-block之间约4px的文本间隙 */ } .family-tree li { display: inline-block; font-size: 14px; }注意font-size: 0只放在外容器上,li内部必须恢复字号,否则成员名字会缩成一个小点。
坑二:最左侧和最右侧子节点的竖线悬空。
现象:分支横线两端各有一根竖线,比中间的短一截,或者连接到一半断掉。
原因:常见的写法是li:first-child::before, li:last-child::after { border: 0 none; }一刀切,把两端节点伸向边的横线整段删了,竖线同时失去了起点,于是悬在半空。
解决:不要整体删,只去掉横向的border-top,保留可能存在的竖向边框逻辑。最左节点的::before设border-top: none,最右节点的::after同理:
.family-tree li:first-child::before { border-top: none; } .family-tree li:last-child::after { border-top: none; }这样左右端头是干净的截断,而不是连竖线一起消失。
坑三:调整垂直间距后,节点和连线开始错位。
现象:改了padding-top想让树更舒展,结果竖线穿过了成员名牌,或者节点和线不在同一条中轴线上。
原因:垂直间距参数散落在多处,li的padding-top、伪元素的height、ul::before的height和top必须同时保持一致,只改一处必然错位。教训就是别让同一组参数散落多处。
解决:统一抽成CSS变量:
.family-tree { --v-gap: 30px; } .family-tree li { padding-top: var(--v-gap); } .family-tree li::before, .family-tree li::after { height: var(--v-gap); } .family-tree ul ul::before { height: var(--v-gap); }以后要改树形松紧度,只动--v-gap这一个值即可,所有相关位置自动同步更新。
坑四:整棵树太宽,在屏幕上被直接截掉。
现象:手机上打开页面,树的右半边看不到,也没有横向滚动条,内容像被裁掉了。
原因:.family-tree容器默认宽度等于视口宽度,内部排完的超宽内容被容器边界裁掉,浏览器也不会自动触发横向滚动。
解决:在树外围包一层滚动容器,把宽度约束转移到外层:
.family-tree-scroll { overflow-x: auto; padding-bottom: 12px; } .family-tree { width: max-content; /* 宽度由内容决定,不受视口限制 */ }这是处理大型族谱最基础的方案,也是不引库前提下唯一靠谱的横向滚动方案。
坑五:伪元素全部不显示,连线一根都没有。
现象:节点卡片正常排列,但整棵树没有任何连线。
原因:伪元素缺少必要声明。::before和::after只有在content不为空、且元素本身定位生效时才会渲染,最常见的错误是漏写content: "",或者父级li没有position: relative导致绝对定位基准丢失。
解决:检查每条伪元素规则是否包含content: ""和position: absolute,再看对应li或ul是否设置了position: relative。这两个条件缺一个,连线就出不来。这个坑最隐蔽,因为浏览器不会报任何错误,整个页面看起来“一切正常”但就是没线。
5. 二次开发思路:从静态家族树升级成交互图谱
如果你照着上面流程跑通了,很快会发现纯静态方案最大的限制是交互:族谱一大,几十个节点全展开在页面上,想快速定位某个分支并不方便。其实不用引入框架,两个方向就能让体验上一个台阶。
5.1 用details元素做可折叠分支,仍然零JavaScript
HTML原生提供了一个展开收起组件<details>,它天生适合套在树形结构上。改造思路是把每个有后代的节点,从原来的“member+ 子ul”结构换成details包裹:
<li> <details open> <summary> <div class="member">曾祖父</div> </summary> <ul> <li><div class="member">长房</div></li> <li><div class="member">次房</div></li> </ul> </details> </li>open属性控制默认状态:有它就展开,去掉就默认收起。用户点击summary区域即可切换子分支的显示与隐藏,完全不需要JavaScript。样式上需要压制默认的三角形图标:summary { list-style: none; },再给summary加一个cursor: pointer和自定义的展开箭头标识,用来暗示这里可以点击。这套做法的好处是无依赖、语义清晰、折叠状态还能通过CSS的details:not([open])选择器自由定制,比如给收起状态加一层透明度。对于要交付给非技术用户维护的族谱项目,这是最不容易翻车的交互升级。
5.2 用模板生成替代手写嵌套:数据驱动的一次过渡
当节点规模到了几十个以上,手写嵌套的缩进会让人崩溃,漏写一个</ul>就能毁掉整棵结构。我一般会在这个阶段把数据抽成JavaScript数组,用一段递归函数生成树形HTML:
const members = [ { id: 1, name: '曾祖父', children: [2, 3] }, { id: 2, name: '长房', children: [4, 5] }, { id: 3, name: '次房', children: [6, 7] }, { id: 4, name: '堂伯', children: [] }, { id: 5, name: '堂叔', children: [] }, { id: 6, name: '父亲', children: [8] }, { id: 7, name: '我', children: [] }, { id: 8, name: '长子', children: [] }, ]; function renderTree(rootId) { const node = members.find(m => m.id === rootId); if (!node) return ''; const childrenHtml = node.children.map(renderTree).join(''); return ` <li> <div class="member">${node.name}</div> ${childrenHtml ? `<ul>${childrenHtml}</ul>` : ''} </li> `; } document.querySelector('.family-tree ul').innerHTML = renderTree(1);这段代码的核心是renderTree的树形递归:先找到当前节点,再对它的children数组逐项调用自身,递归天然对应了树形的嵌套结构。children数组就是整个数据层,跟HTML结构彻底解耦。将来如果要做编辑功能,只需要在数据层增删节点,再调用一次renderTree重新渲染即可。这也意味着项目从静态方案向数据驱动方案过渡时,不需要换掉原有CSS,树的呈现层可以完整保留。
每个项目都有它的边界,纯CSS树形结构也不例外。但从实际经验来看,静态族谱场景下它的表现远超预期——零依赖、易维护、高定制,唯一的代价是你要愿意接受“数据写在HTML里”这件朴素的事。从那以后我每次交付这类树形页面,都强制自己先把间距变量、:only-child边界、左右端点截断这三件事写入样式表,再开始调颜色和字体,踩坑记录里的前三条就是这么攒下来的。希望这套模板能帮你少走几段弯路,早日画出属于自己的第一棵家族树。希望帮到你。
本文还有配套的精品资源,点击获取