表格与列表全解:从语义化选型到CSS响应式实战
带过几位刚入行的前端同学发现一个挺普遍的现象大家上手 Vue 或 React 之后写页面习惯了div一把梭真到要做数据展示、信息罗列的时候反而把最基础的表格和列表用得很混乱。有人用table做了导航菜单有人用ul li拼了一个商品对比面板还有人把表格的语义标签thead、tbody全丢一边写完才发现选中样式根本不好控制。表格和列表是前端页面里最基础、也最容易被忽略的两个知识点。它们看似简单实际上背后关系到信息层级、语义化、样式可维护性、响应式适配等多个层面的问题。这篇文章我想把这两块内容完整地拆一遍从什么时候该用表格、什么时候该用列表这个选型问题开始到标准结构、合并单元格、嵌套规则再到 CSS 改造的实操技巧和移动端降级方案最后配一个能直接抄作业的综合练习。无论你是刚接触前端的小白还是写了几年页面但一直靠div硬撑的开发者这篇应该都能帮你把地基补扎实。1. 先分清什么时候用表格什么时候用列表先说最容易踩的坑选错标签。前阵子有位同学拿一个商品规格页给我看他把商品的名称、价格、库存、销量四项信息用ul li横向排列说是仿照设计稿做成四个小卡片。设计稿确实长这样但问题是这份数据本质上是一个二维结构——每一行是一个商品每一列是一个属性。用列表做视觉上能模仿个大概但后续一旦要排序、要固定表头、要做斑马纹CSS 会写得异常痛苦。判断标准其实就一条数据有没有行列交叉的关系。商品列表每个商品信息天然是一行包含多个属性列用table。排行榜 Top 10名次只是顺序不是属性列用ol。导航菜单只有一项接一项的平级关系用ul。商品参数对比多行多列行列交汇处才是某个商品的某个参数用table。名词解释、FAQ 列表一个术语对应一段描述用dl。1.1 列表的本质纵向信息流列表的核心特征是纵向罗列它表达的是一组同类事物的先后或主次关系。ul是无序的适合表达集合感比如导航、标签、缩略图组ol是有序的适合表达流程步骤、排名、操作顺序。我记得很早之前做过一个后台的日志筛选面板把筛选条件用ul li列出来每项前面加一个图标后面加一个删除按钮。这类条目式的信息天然是列表的活硬要用表格反而会在响应式阶段给自己找麻烦。1.2 表格的本质二维交叉数据集表格是用来承载二维数据的。它的特征是有行、有列、有表头行和列的交叉点才是具体数值。只要你的数据需要换个维度看比如按季度看每个地区的销量——这就是妥妥的表格场景。很多人觉得表格是老古董其实不然。管理后台的数据报表、电商后台的订单明细、商品参数对比、排班表、课程表这些场景里表格依然是最清晰、最可访问的方案。display: grid虽然也能排版成网格但它在语义和屏幕阅读器友好程度上完败给真实表格。1.3 一个快速自测方法我在实际带人的时候习惯让同学做一个自测如果数据去掉所有样式只剩纯文本后它的结构是一列一列往下排还是一行一行带着多个属性往下排前者用列表。后者用表格。如果既有分类标题、又有描述文字比如处理器八核这种键值对用dl。这套自测方法基本能覆盖绝大多数场景判断对了后面的代码写起来会顺手很多。2. 表格结构拆解从caption到tfoot每个标签都不是摆设表格这块我见过太多人只用了table、tr、td三个标签剩下的全靠 CSS 硬撑。其实 HTML 给表格设计了一套相当完整的语义结构用好了不仅能提升可访问性还能让样式控制变得异常轻松。2.1 标准表格的骨架一个语义完整的表格长这样table caption2024年某产品线季度销量对比/caption thead tr th scopecol季度/th th scopecol销售额/th th scopecol同比增长/th /tr /thead tbody tr th scoperowQ1/th td128.6万/td td12.3%/td /tr tr th scoperowQ2/th td142.8万/td td8.7%/td /tr tr th scoperowQ3/th td158.2万/td td15.2%/td /tr /tbody tfoot tr th scoperow全年合计/th td429.6万/td td11.8%/td /tr /tfoot /table逐个说下每个标签的作用caption表格的标题默认显示在表格上方。它不只是个标题这么简单它是表格的aria-labelledby屏幕阅读器会先读它让用户知道这个表格在讲什么。thead表头分组。把表头单独包一层好处是样式上可以整体控制比如固定表头背景色还有一点很重要——分页打印时表头会在每一页重复出现。tbody表格主体。一个页面可以存在多个tbody用于把数据按逻辑分组配合display: table-header-group之类的属性还能实现一些特殊的滚动效果。tfoot表尾。注意它的位置在 HTML 源码中可以写在tbody前面浏览器渲染时会自动放到表格底部。适合放合计、汇总之类的内容。th表头单元格有scope属性col表示列方向row表示行方向。这个属性对视力障碍用户非常重要能告诉读屏软件这个表头管辖的是哪一行还是哪一列。td普通数据单元格。2.2 为什么语义结构对样式这么重要很多同学觉得语义化是虚的其实不是。至少在表格这个场景里语义化直接决定了你写 CSS 的难度。举个例子你想让表头和表尾的背景色一样深、数据区加斑马纹如果thead、tbody、tfoot都在直接三行选择器搞定thead th, tfoot th { background: #f0f2f5; } tbody tr:nth-child(even) { background: #fafbfc; }如果全是一堆没有分组的tr混在一起你想区分表头行和数据行就只能靠加 class。加 class 不是不行但维护成本是实打实的——结构固定住之后语义标签本身就是最好的 class。2.3 关于废弃属性写表格的时候还有几个老属性需要留意一下。以前常用align、bgcolor、cellpadding、border这些属性控制样式如今它们已经被 CSS 完全替代HTML 规范里也标记为不推荐使用。但话说回来在某些邮件客户端里这些老属性反而活得比 CSS 好比如做 HTML 邮件时用bgcolor和cellpadding仍然是常见做法。这属于特例做后台系统的同学不用太纠结。还有一个基本废弃的属性是summary以前用来给表格加摘要描述现在建议直接用caption或表格外的文本替代。3. 合并单元格与列表嵌套页面里最容易被写错的部分这个章节我单独拎出来是因为合并单元格和列表嵌套是新手写表格和列表时出错率最高的两处。3.1 rowspan与colspan的合并逻辑先看一个典型的合并场景——套餐对比表table thead tr th scopecol套餐类型/th th scopecol包含内容/th th scopecol额度/th /tr /thead tbody tr td rowspan2基础套餐/td td国内流量/td td30GB/td /tr tr td语音通话/td td200分钟/td /tr tr td rowspan2高级套餐/td td国内流量/td td60GB/td /tr tr td语音通话/td td500分钟/td /tr /tbody /tablerowspan2表示这个单元格要往下跨两行。关键点是被它吃掉的那一行里就不要再写这个格子了。上面代码里第二个tr里只有两个td第三个格子已经被第一行的td占掉了再写就多出一列整个表格的列数会对不齐。colspan同理是横向合并列。比如表头要做两个层级tr th rowspan2项目/th th colspan2上半年/th th colspan2下半年/th /tr tr thQ1/th thQ2/th thQ3/th thQ4/th /tr合并单元格最容易踩的坑就是数格子。我一般建议写完结构后先在浏览器里数一下每一行的实际列数确保它们相等。还有一种验证技巧是全选表格内容看选中区域的形状是不是规则的矩形——如果出现台阶状选区基本就是合并出了问题。3.2 列表嵌套的两个合法姿势以前面试前端的时候我常问一个问题导航菜单的二级下拉用嵌套列表怎么写很多同学会写出这样的代码ul li父级菜单/li ul li子菜单一/li li子菜单二/li /ul /ul这是错误的写法。ul不能直接作为ul的子元素它必须被包在li里。正确的嵌套结构是ul li 父级菜单 ul li子菜单一/li li子菜单二/li /ul /li /ul嵌套列表的本质是子列表属于某个列表项所以ul或ol只能出现在li内部。这个结构不仅语义正确也直接影响样式层级——二级列表缩进、小圆点样式等都依赖这种合法的包裹关系。ol的嵌套也遵循同样的规则。另一个常见问题是层级缩进默认ul有padding-left: 40px多层嵌套后缩进会特别深想控制层级间距推荐的做法是ul, ol { padding-left: 0; list-style: none; } li ul, li ol { margin-left: 16px; }这样每一层只缩进固定间距视觉上干净很多。3.3 自定义列表dl的冷门但实用dldescription list在实战里的出场率比ul、ol低很多但它有个独家用途键值对信息。商品详情页的规格参数、账号设置页的昵称/手机号/绑定邮箱这类信息本质都是一个名词 一段说明的结构用dl最合适dl dt屏幕尺寸/dt dd6.7英寸/dd dt电池容量/dt dd4800mAh/dd dt充电规格/dt dd120W 有线快充/dd /dldl的实际应用比大家想象中多。CMS 系统的字段展示、小程序个人信息中心页面我用dl写过不少好处是结构一目了然而且dt天然适合做统一的标签列宽控制。这里补充一点虽然 HTML 规范曾讨论过dl能否容纳多个dt对应一个dd实际使用中一个dt带一个dd是最清晰稳妥的遇到一个名词多个值的情况比如同一插件支持多个参数名拆成多个dt即可。4. 让默认长相变好看表格与列表的CSS改造要点写前端的人都有一个共识浏览器的默认样式太难看了。表格默认有双线边框列表默认带圆点和缩进。这节讲怎么把它们改造成真正能上线的组件。4.1 表格一套可复用的现代化样式先说最核心的三个要点。第一合并边框。默认表格的边框是分开的单元格之间会有缝隙丑且不好控制。border-collapse: collapse能解决这个问题它让相邻单元格的边框合并成一条线。注意它要求边框必须定义在table或td/th上不能定义在tr上。第二结构性边框。现代表格风格不再依赖四周全部框线更常见的做法是下边框 斑马纹.table-wrapper { overflow-x: auto; } .table-wrapper table { width: 100%; border-collapse: collapse; font-size: 14px; } .table-wrapper th, .table-wrapper td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #e5e7eb; } .table-wrapper thead th { background: #f8fafc; font-weight: 600; white-space: nowrap; } .table-wrapper tbody tr:nth-child(even) { background: #fafbfc; } .table-wrapper tbody tr:hover { background: #f0f4ff; }这里有个细节nth-child(even)的斑马纹配合 hover 高亮视觉反馈很好但注意斑马纹的颜色要足够浅否则 hover 颜色上去之后看不出变化。第三对齐规则。文本默认左对齐数字默认也应该左对齐不一定。表格里的数值列建议右对齐这样一列数字的位数能直观对比th保持左对齐但操作这类列要右对齐。经验法则文本列左对齐数值列右对齐布尔状态列如已启用/已停用居中对齐用 CSS 统一写td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }tabular-nums让数字使用等宽数字字形小数点和位数对齐后读起来会舒服很多。4.2 table-layout一个经常被忽略的属性表格有一个默认的自动布局算法它会根据每一列的内容去计算最优列宽内容一多性能就下降而且列宽不可控。手动设置table-layout: fixed之后列宽由第一行通常是表头决定或者由colgroup显式指定内容过长不会影响整体布局而是被overflow处理。table { table-layout: fixed; width: 100%; } td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }这个属性在数据量大、列数多的表格上收益非常明显。做后台系统的时候列多、行也多自动布局每次渲染都要扫描所有单元格来计算宽度fixed直接省掉这一步。4.3 列表的样式控制list-style与marker列表的样式控制核心也是两点。第一干掉默认标记。导航和卡片列表里默认的小圆点基本都会被移除ul { list-style: none; padding: 0; margin: 0; }注意list-style: none不只是去掉小圆点还应该配合padding: 0。否则不同的浏览器默认缩进不一样页面在不同设备上会跑偏。以前还有老浏览器必须写list-style-position: inside才能让项目符号不超出边界现在不需要了。第二用自定义标记替代。去掉默认圆点不代表不能有可视化标记。我常用的方式是::before伪元素.list-item { display: flex; align-items: center; gap: 8px; } .list-item::before { content: ; width: 8px; height: 8px; border-radius: 50%; background: #2c7be5; }相比默认圆点这种方式对颜色、大小、对齐的控制都是完全自由的。需要注意li默认是list-item的display设置flex后会丢失项目符号所以这里不用考虑::marker的干扰。如果希望保留列表的语义又能用图片当项目符号可以配合::marker不过::marker能控制的样式有限实际项目里还是::before更实用。4.4 横向列表导航菜单的常见实现把纵向列表变成横向导航最简单的方式是display: flexul classnav lia href#首页/a/li lia href#产品/a/li lia href#关于/a/li /ul.nav { display: flex; list-style: none; padding: 0; gap: 24px; } .nav a { text-decoration: none; color: #333; padding: 8px 12px; border-radius: 6px; } .nav a:hover { background: #f0f4ff; color: #2c7be5; }这里有个容易被忽略的细节li里的a是行内元素直接设置padding和border-radius能生效但垂直方向的padding不会撑开父容器。想让导航项的可点击区域变大且整齐可以给a设置display: inline-block或者干脆在li上做padding。我在项目里习惯在li上做点击区域控制然后让a完全填充。5. 小屏设备上的表格三种实战降级方案表格在小屏设备上的体验问题几乎是每个后台项目都逃不过的坎。手机上看一个十几列的订单表格如果不做任何处理横向滚动是肯定的但横向滚动到底能不能接受要看场景。5.1 方案一外层滚动容器最简单的降级方案就是包一层.table-wrapper配合overflow-x: autodiv classtable-wrapper table.../table /div.table-wrapper { overflow-x: auto; -webkit-overflow-scrolling: touch; border: 1px solid #e5e7eb; border-radius: 8px; }这个方案适合数据列固定、不需要重新组织的场景。为了滚动体验好一点可以配合thead th { position: sticky; top: 0; }让表头在纵向滚动时固定。但这个方案有两个小坑border-radius在滚动时边框会被内容盖住需要在.table-wrapper里加一层background或者给表格本身设圆角。如果表格外面还有卡片容器卡片的 padding 会加宽滚动条位置视觉上不如全宽的滚动流畅。5.2 方案二卡片化重构如果表格的数据列很多横向滚动会让人转晕那可以考虑把一行数据变成一张卡片。做法是用 CSS 把每个td变成display: block并用>media (max-width: 640px) { .table-wrapper table, .table-wrapper thead, .table-wrapper tbody, .table-wrapper tr, .table-wrapper th, .table-wrapper td { display: block; } .table-wrapper thead { display: none; } .table-wrapper tr { margin-bottom: 16px; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; } .table-wrapper td { display: flex; justify-content: space-between; border-bottom: none; padding: 6px 0; } .table-wrapper td::before { content: attr(data-label); font-weight: 600; color: #6b7280; margin-right: 12px; } }对应的 HTML 里每个td都要带>td>media (max-width: 768px) { .col-secondary { display: none; } }在 HTML 里给次要列的表头和所有数据单元格都加上col-secondary类。这个方案适合列多、但用户真正关心的只有三四列的场景比如订单列表手机上只需要看订单号、状态和金额。三种方案选择的标准我一般这么判断数据量大、需要完整对比方案一。列数不多但每列信息独立性强方案二。列太多、需要优先保证核心信息方案三或者方案三 详情页兜底。6. 综合练习做一个商品信息页里的表格与菜单列表理论讲完最后配一个综合练习。这个练习是模拟某电商后台的商品信息管理页核心包含三个区域页面的侧边导航菜单列表、商品主数据表格表格、以及商品详情处的键值对参数dl。做完这个练习表格和列表的核心知识点基本就全部覆盖了。6.1 需求与结构规划页面结构分三块左侧垂直导航用ul li实现商品管理的二级菜单。右侧商品列表用table展示商品名称、分类、价格、库存、状态。点击某行商品在下方展示该商品的参数信息用dl实现品牌 / 型号 / 保修期等键值对。这三块正好对应了列表、表格、自定义列表三种场景。先说 HTML 结构aside classsidebar ul classmenu li 商品管理 ul classsubmenu lia href#商品列表/a/li lia href#新增商品/a/li lia href#分类管理/a/li /ul /li lia href#订单管理/a/li lia href#客户管理/a/li /ul /aside main classcontent div classtable-wrapper table classproduct-table caption商品库存一览/caption thead tr th scopecol商品名称/th th scopecol分类/th th scopecol classnum价格/th th scopecol classnum库存/th th scopecol状态/th th scopecol操作/th /tr /thead tbody tr td>body { display: flex; min-height: 100vh; margin: 0; } .sidebar { width: 220px; background: #1f2937; padding: 16px 0; } .content { flex: 1; padding: 24px; background: #f3f4f6; }第二步菜单样式。清除列表默认样式二级菜单缩进并隐藏hover 时展开.menu, .submenu { list-style: none; padding: 0; margin: 0; } .menu a { display: block; color: #d1d5db; text-decoration: none; padding: 10px 20px; } .menu a:hover { background: #374151; color: #fff; } .submenu { display: none; margin-left: 16px; } li:hover .submenu { display: block; }这个实现里用到了li:hover .submenu注意这里的选择器是直接子元素如果子菜单不是li的直接子元素而是嵌套在li里的ul它是直接子元素没问题。但这个写法只适用于纯 CSS 的 hover 展开如果要做点击展开需要配合 JavaScript 切换 class。第三步表格样式。直接复用上一节的高质量表格 CSS包括斑马纹和 hover 效果。另外把价格、库存列加上text-align: right。第四步移动端响应式。用media把表格切换成卡片化方案方案二同时把侧边栏改到顶部或折叠。media (max-width: 640px) { body { flex-direction: column; } .sidebar { width: 100%; } .table-wrapper table, .table-wrapper thead, .table-wrapper tbody, .table-wrapper tr, .table-wrapper th, .table-wrapper td { display: block; } .table-wrapper thead { display: none; } .table-wrapper tr { margin-bottom: 12px; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px 12px; } .table-wrapper td { display: flex; justify-content: space-between; border-bottom: none; padding: 6px 0; } .table-wrapper td::before { content: attr(data-label); font-weight: 600; color: #6b7280; } }6.3 练习检查清单做完之后建议对照下面这份清单自查一遍[ ] 列表是否用了合法的嵌套结构ul在li内部[ ] 表格是否包含caption、thead、tbody[ ]th是否设置了scope[ ] 合并单元格后每行列数是否一致[ ] 表格数值列是否右对齐[ ] 移动端下表格是否变成了卡片模式[ ] 是否去掉了浏览器默认的列表缩进和圆点如果这些条件全部满足说明表格和列表这块你已经过关了。在此基础上还可以继续扩展试试用 JavaScript 给表格排序、给列表加拖拽排序、用dialog做一个新增商品弹窗把表格和表单串起来。前端最底层的这些语义标签扎实掌握之后写复杂页面反而会更轻松——因为你知道每种信息该用什么容器去装它。