
简介面向 HTML 初学者与网页制作入门者的表格练习源码包围绕表单与表格的搭建展开适合课堂作业、自学练手与教学演示等场景帮助读者理解表格结构、行列组织及基础样式呈现方式。压缩包共 5 个文件约 30KB以 3 张 jpg 效果图、1 个 html 页面和 1 份 md 说明文档为主图片用于对照页面呈现效果html 承载表格与表单的主体代码md 文档补充使用说明体量轻便便于快速查阅与二次修改。已有 203 人学习具备一定的入门参考价值。读者可从中获得可直接打开的表格页面示例、页面效果对照素材以及简要的说明指引用于模仿结构、调整单元格与表单元素并在此基础上完成自己的表格练习整体目录简洁适合作为 HTML 基础阶段的小型案例参考。1. 一个几十行的 HTML 表格源码包值不值得单独下载后台页面上线前一天某开发者发来一张截图两个表格之间隔着一道双线边框怎么改都改不掉。他手里那个 HTML 制作简单表格的源码包本身没问题问题是他又叠了三层自己写的样式。这类压缩包解开通常是 index.html 加一份 style.css几十行 HTML、一两百行 CSS看着简陋但结构摆得对thead 与 tbody 分层、colgroup 管列宽、合并单元格的边界留得干净。它的价值不在「能显示数据」而在于给了一个不和后续样式冲突的基础。适合三类人刚开始写前端、要快速交一个能看的数据表的做后台页面需要临时贴表格骨架的以及被各种表格组件限制住、想弄清原生 table 写法的。下面按拆包、跑起来、调参、排坑、进阶走一遍。2. 解压看结构table 的语义骨架决定了后面所有样式2.1 目录里那几个文件各自的职责这类包解开之后常见组合是 index.html、一个 css 目录或直接一个 style.css偶尔带一个 script.js再配一两张演示截图。别嫌它文件少表格这种「结构即样式」的组件文件少反而是优点——你能一眼看清哪一层在管什么。我拿到手第一件事不是打开浏览器预览而是做一次「拆分」把 index.html 里 table 那一段整块复制出来另存成 demo.htmlCSS 只保留所有以 table、th、td、tr、thead、tbody 开头的选择器其余规则先注释掉。这一步花不到五分钟但能立刻判断出这套源码的样式是「挂在 table 上」还是「挂在一堆 div 包裹层上」。前者可以放心搬进项目后者搬的时候必须连包裹结构一起搬耦合度高你改一个内边距可能牵连三个类名最后发现某个间距其实来自最外层 div 的 padding。HTML 部分要重点看的不是 class 有多少而是三件事表头有没有放进 thead、数据行是不是都在 tbody 里、有没有 caption。这三样决定了你后面做粘性表头、做斑马纹、做无障碍朗读时要不要返工重写。我见过不少标着「简单表格」的源码整个表就是一串平铺的 tr表头也是 tr只是把 td 换成 th。视觉上完全正常可你只要想给表头加 position: sticky或者想用 JS 只对 tbody 做排序就得先把结构拆开。CSS 部分只看两条有没有 border-collapse有没有 table-layout。这两条几乎决定了后面所有列宽和边框问题的走向第 4 章会展开讲具体取值。2.2 语义骨架从 caption 到 tfoot 该摆什么先把一份合格的骨架摆出来后面所有改造都基于它。!-- demo.html先不接任何框架把结构跑通 -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 !-- 移动端必写否则 width:100% 的表格会被整页缩放 -- meta nameviewport contentwidthdevice-width, initial-scale1 title订单明细/title link relstylesheet hrefstyle.css /head body !-- 滚动容器横向滚动和粘性表头都挂在它身上不要挂在 table 上 -- div classtable-wrap table classdata-table caption第三季度订单明细/caption !-- colgroup 只负责列宽和列级样式不参与数据 -- colgroup col classcol-no col classcol-name col classcol-qty col classcol-price /colgroup thead tr !-- scope 让读屏软件知道这是列头不是普通单元格 -- th scopecol序号/th th scopecol商品名称/th th scopecol数量/th th scopecol金额元/th /tr /thead tbody trtd1/tdtd机械键盘/tdtd2/tdtd598.00/td/tr trtd2/tdtd显示器支架/tdtd1/tdtd329.00/td/tr trtd3/tdtd扩展坞/tdtd3/tdtd447.00/td/tr /tbody tfoot !-- colspan3 把前三列合并成「合计」标签 -- trtd colspan3合计/tdtd1374.00/td/tr /tfoot /table /div /body /html逻辑说明外层 div 是滚动容器这一点很多现成源码会漏掉直接把 overflow 写在 table 上结果粘性表头失效第 5 章会讲原因。caption 放在 table 的第一个子元素位置它既是视觉标题也是读屏软件朗读这张表的入口。colgroup 里每个 col 对应一列只写宽度和列级类名不写内容。参数说明scopecol 适用于列头行头用 scoperow如果表头结构复杂两行表头、合并表头scope 表达不了要改用 headers 属性给每个 td 写上对应表头的 id形如td headersh-price h-region。这个属性写起来麻烦但它是复杂表头唯一能被读屏正确解析的方案。2.3 为什么不用 div 拼表格三个具体代价每次有人问「表格用 div 写是不是更灵活」我的回答都一样灵活是真的代价也是真的。第一个代价是对齐。用 div 拼的表格行高对齐要靠固定高度或 flex 撑一旦某列内容换行成两行整行的垂直对齐就开始抖。原生 table 的自动行高对齐是浏览器排版引擎内建行为你不需要写一行代码。第二个代价是无障碍和复制粘贴。用户从网页上框选一张 div 表格复制到 Excel 里得到的通常是一坨没有分列的文本原生 table 复制过去能自动分列分单元格。同理读屏软件对 div 表格的解析基本靠猜。第三个代价是打印和导出。table 在打印样式里有 thead 跨页重复的能力配合 display: table-header-groupdiv 方案要实现得靠自己算分页。如果这份表格最终会被打印或转 PDF别犹豫用 table。代价反过来讲也成立如果你的「表格」其实是用来做页面布局的左栏导航右栏内容那种那它本来就不该是 table。判断标准很简单——这张表有没有「行」和「列」的语义概念。有就用 table。3. 四步把静态表格跑成能用的数据表3.1 第一步让边框不重影的 CSS很多人拿到源码后第一个动作是给 td 加 border然后发现相邻单元格的边框叠在一起变成两像素。原因是 table 默认的边框模型是 separate每个单元格各画各的框中间还夹着 border-spacing。/* style.css先只留控制表格的规则其余全部注释掉 */ .table-wrap{ width: 100%; overflow-x: auto; /* 窄屏横向滚动而不是把列压扁 */ -webkit-overflow-scrolling: touch; } .data-table{ width: 100%; border-collapse: collapse; /* 关键一合并相邻边框消掉双线 */ table-layout: fixed; /* 关键二列宽按 colgroup 分配不被内容撑开 */ font-size: 14px; color: #1d2129; } .data-table caption{ caption-side: top; /* 标题在表上方默认就是 top写出来更明确 */ text-align: left; padding: 8px 12px; font-weight: 600; } .data-table th, .data-table td{ padding: 10px 12px; border-bottom: 1px solid #e5e6eb; /* 只画下边框视觉最干净 */ text-align: left; vertical-align: middle; word-break: break-word; /* 长串强制换行防撑破 */ } .data-table thead th{ background: #f7f8fa; font-weight: 600; white-space: nowrap; /* 表头不换行宽度由 colgroup 兜底 */ } /* colgroup 上的类名会作用到整列 */ .data-table .col-no{ width: 64px; } .data-table .col-qty{ width: 90px; } .data-table .col-price{ width: 120px; } /* 数字列右对齐 等宽数字位数上下对齐才好扫读 */ .data-table td:nth-child(3), .data-table td:nth-child(4), .data-table tfoot td:last-child{ text-align: right; font-variant-numeric: tabular-nums; } .data-table tbody tr:nth-child(even){ background: #fafbfc; } .data-table tbody tr:hover{ background: #eef4ff; }逻辑说明border-collapse: collapse 把相邻单元格的边框合并成一条这是治双线的唯一正解用负 margin 或者给单个方向加 border 都是绕路。table-layout: fixed 让浏览器按第一行或 colgroup的宽度直接分配不再等内容加载完再算一次长表格的渲染性能也会好一截。参数说明border-collapse 有两个值separate 时 border-spacing 才生效collapse 时 border-spacing 被忽略。table-layout: fixed 有个前提——表格必须有确定的宽度width: 100% 或具体像素否则它会退化成 auto。如果某列内容特别长又不想换行把那一列的 td 单独设 white-space: nowrap别在全局开。3.2 第二步colspan 与 rowspan 的边界控制合并单元格是表格里最容易出错的部分不是语法错是视觉和逻辑错位。!-- 两行表头 跨行分类的典型结构 -- table classdata-table colgroup col classcol-region col span2 !-- span2 表示这一条 col 定义同时作用于两列 -- /colgroup thead tr th scopecol rowspan2区域/th !-- 纵向占两行 -- th scopecolgroup colspan2销量/th !-- 横向占两列 -- /tr tr th scopecol线上/th th scopecol线下/th /tr /thead tbody tr th scoperow华东/th !-- 行头用 th scoperow不是 td -- td1280/td td960/td /tr tr th scoperow华南/th td1105/td td842/td /tr /tbody /table逻辑说明合并的方向要想清楚——colspan 是横着吃列rowspan 是竖着吃行被吃掉的位置在后续行里不能再写对应的 td否则整行会多出一格后面的列全部右移。这是个一眼看不出来的错因为浏览器不会报错只是默默错位。参数说明col 标签的 span 属性是「这条定义复制给几列」和 td 的 colspan 不是一回事写混了列宽会莫名其妙。合并后的表头如果结构复杂到 scope 表达不了就得用 headers id 的方案把每个数据格和它的表头绑起来。另外合并单元格会直接影响斑马纹和排序第 5 章有专门的排查条目。3.3 第三步窄屏的两条路横向滚动还是卡片化移动端是表格绕不过去的坎。两条路各有代价。第一条是横向滚动改动最小media (max-width: 640px){ .table-wrap{ overflow-x: auto; } .data-table{ min-width: 560px; } /* 给个下限列不会被压成一条 */ }第二条是卡片化读起来舒服但语义会丢!-- 每个 td 上补一个>media (max-width: 640px){ .table-wrap{ overflow-x: visible; } .data-table{ table-layout: auto; } /* 表头视觉隐藏但不给 display:none尽量保留给读屏 */ .data-table thead{ position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } .data-table, .data-table tbody, .data-table tr, .data-table td{ display: block; } .data-table tr{ border: 1px solid #e5e6eb; border-radius: 8px; margin-bottom: 12px; padding: 4px 0; } .data-table td{ display: flex; justify-content: space-between; gap: 16px; border: none; padding: 8px 12px; } /* 标签从>/* 前提一滚动容器要有确定的高度上限让容器自己滚 */ .table-wrap{ max-height: 480px; /* 超过就容器内滚动而不是整页滚 */ overflow: auto; } /* 前提二border-collapse 必须换成 separate否则 sticky 时边框会跟着滚 */ .data-table{ border-collapse: separate; border-spacing: 0; /* 用 0 间距补偿视觉上和 collapse 一致 */ } .data-table thead th{ position: sticky; top: 0; z-index: 2; /* 高于普通单元格防止被内容盖住 */ background: #f7f8fa; /* 必须有背景色否则滚动时内容会透出来 */ /* separate 模式下补回下边框collapse 的下边框在 sticky 时会丢 */ box-shadow: inset 0 -1px 0 #e5e6eb; }逻辑说明sticky 是相对「最近的滚动祖先」定位的。如果滚动容器在垂直方向不滚动只是 overflow-x: auto表头就没有可粘的参照表现就是完全不生效。所以要让容器自己可滚给它 max-height 或 height。参数说明border-collapse: separate 之后相邻边框不再合并需要用 border-spacing: 0 把间距归零再用 box-shadow: inset 模拟下边框因为 collapse 模式下的边框在 sticky 元素上有渲染缺陷。z-index 给 2 是经验值只要比普通 td 的层级高就行不需要给很大。4.2 列宽控制的四种手段与优先级列宽打架是表格调样式时最常遇到的问题四个手段按优先级从高到低是手段写法生效条件适用场景col 宽度col classcol-no.col-no{width:64px}table-layout: fixed整列统一宽度推荐首行单元格宽度th:first-child{width:64px}table-layout: fixed没写 colgroup 时的补救min-width / max-widthtd{min-width:80px}任意只做下限保护不精确控宽内容撑开不写宽度table-layout: auto列宽随内容自适应长表格会跳优先级上colgroup 里的 col 宽度会覆盖首行单元格的宽度声明而 table-layout: auto 时两者都可能被内容顶掉。所以我的习惯是先定 table-layout: fixed再在 colgroup 里把固定列序号、状态、操作写死剩下的列不给宽度让它们按比例分剩余空间这样窗口拉伸时只有弹性列在变固定列纹丝不动。要注意 width 在 fixed 模式下是按比例分配的如果你给四列分别写 100px、剩余两列不写那剩余空间会被这两列均分。如果只给一列 200px其余不给那 200px 是硬性的剩下的按内容最短需求分——这也是为什么很多人发现写了宽度却没生效其实是第一行的某个单元格内容太宽把整列顶开了。4.3 斑马纹、悬停与打印的参数表CSS 属性常用取值说明border-collapsecollapse / separatecollapse 消双线separate 才能配 stickyborder-spacing0 / 0 8px只在 separate 下生效横向纵向可分开写table-layoutfixed / autofixed 性能好、列宽可控auto 随内容caption-sidetop / bottom标题位置较少改empty-cellsshow / hideseparate 下空单元格是否画边框vertical-alignmiddle / top单元格内容垂直对齐默认 middleword-breakbreak-word / break-all长串换行break-all 会切断英文单词white-spacenowrap / normal表头常设 nowrap数据列不建议打印场景单独一段media print{ .table-wrap{ overflow: visible; max-height: none; } .data-table{ font-size: 12px; } /* 让 thead 在跨页时重复出现在每页顶部 */ .data-table thead{ display: table-header-group; } .data-table tfoot{ display: table-footer-group; } /* 行内避免被分页切断 */ .data-table tr{ break-inside: avoid; } .data-table tbody tr:hover{ background: transparent; } }逻辑说明display: table-header-group 是 thead 的默认值但在某些重置样式里会被改成 block打印前显式写回来最稳。break-inside: avoid 防止一行数据被撕成两半落在两页上。参数说明打印样式里要记得把 overflow 和 max-height 清掉否则打印出来只有一屏的内容剩下的全被裁掉了——这个坑我在第一次做报表打印时踩过导出的 PDF 只有三行。5. 排查清单五个把我卡过半天的表格问题5.1 斑马纹在合并单元格后错位现象加了 colspan 的行之后下面所有行的条纹颜色都反了本该是灰底的行变成白底而且怎么调 nth-child 的奇偶都没用。原因nth-child 是按 DOM 里的兄弟节点序号算的不是按「视觉上的第几行」。合并单元格导致某些行的实际单元格数量和其他行不一样但 tr 的数量没变——真正的问题往往是你把 tfoot 里的 tr 也算进了 nth-child 的统计范围或者中间插了一行隐藏行display: none 的行仍然占 nth-child 的序号。解决把条纹选择器限定在 tbody 里写成.data-table tbody tr:nth-child(even)别在 table 上直接写 tr:nth-child。如果确实有隐藏行改用 JS 遍历可见行加类名或者用:nth-child(even of :not([hidden]))这种较新的语法支持度要先查。我的做法是老老实实加一个 .is-striped 类用 JS 在渲染完成后按可见行重新打标比跟选择器较劲省时间。5.2 粘性表头突然不粘了现象本地写的时候表头吸顶正常加了个外层容器之后彻底不动了。原因sticky 依赖最近的滚动祖先。只要祖先链上任何一层出现 overflow: hidden、overflow: auto、overflow: scroll、transform、filter粘性参照物就会变。最常见的是某个布局组件给内容区套了overflow: hidden做圆角裁切或者给页面加了transform: translateZ(0)做硬件加速。解决打开开发者工具从 th 往上逐层看 computed 里的 overflow 和 transform把不是必需的那一层去掉。去不掉的话就改成让那层容器自己成为滚动容器并设置高度。这个排查过程挺「玄学」的我的笨办法是在控制台里循环遍历祖先节点把带 overflow 或 transform 的全部打印出来一眼就能定位。5.3 table-layout: fixed 之后列宽失控现象写了 colgroup 和 col 宽度某几列还是被内容顶得很宽或者反过来被压成一条缝。原因fixed 模式下列宽由第一行的单元格决定而 colgroup 的宽度优先级虽然更高但如果表格总宽度不够分配所有固定列加起来超过容器宽度浏览器会按比例压缩表现为「写了也没用」。另一种情况是表格没有确定宽度fixed 静默退化成 auto。解决先确认 table 上有 width: 100% 或具体值再把固定列的宽度加起来确保不超过容器宽度。如果确实需要更多列就别全写死留至少一列不给宽度做弹性列。检查方法是在控制台里选中 table看 computed 的 table-layout 是不是真的是 fixed。5.4 长英文串或 URL 撑破表格现象一行里有个很长的英文标识或者链接整张表被撑出容器出现横向滚动条其他列全被挤扁。原因浏览器默认只在空格和连字符处换行连续的英文和数字串没有断点会把单元格撑到内容宽度。table-layout: fixed 只能约束列的初始宽度不能阻止内容溢出。解决给 td 加word-break: break-word或overflow-wrap: anywhere。不建议用word-break: break-all它会把正常的英文单词也随处切断可读性差很多。如果是 URL配合word-break: break-all只加在那一个类上别全局开。另外给 td 加overflow: hidden; text-overflow: ellipsis; white-space: nowrap也是一种方案但那就变成截断显示了需要配合 title 属性让用户能看到全文。5.5 用 innerHTML 拼行导致事件全丢现象表格数据是动态渲染的每行末尾有个删除按钮第一次渲染点击正常重新渲染一次之后按钮全点不动了。原因用tbody.innerHTML ...拼接新行时浏览器会把整个 tbody 的内容重新解析一遍原有的 DOM 节点被销毁重建之前用 addEventListener 绑上去的监听器自然就没了。解决要么改成tbody.insertAdjacentHTML(beforeend, html)只追加不重建要么用事件委托把监听绑在 tbody 上通过e.target.closest(button)判断点的是哪个。后者更稳动态增删行都不用重新绑。顺带一提拼 HTML 字符串时如果数据来自用户输入记得转义否则就是个现成的注入漏洞——这类源码包里的 script.js 通常不会帮你处理这一步。6. 进阶用>// 表头加 style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />