简介一套面向网页设计初学者与前端入门学习者的HTML表格练习源码围绕表格与表单的基础搭建展开适合课堂作业、课后练习或自学时对照参考。压缩包共5个文件体积约30KB以1个html页面为主体配合3张jpg图片素材和1份md说明文档分别承担页面结构、配图素材与使用说明的作用代码与素材分离存放可直接在浏览器中打开查看效果并逐段修改。已有203人学习下载可作为基础练习的参考样本。读者能拿到一份可运行的表格页面示例据此理解表格行列组织、单元格合并、边框与间距设置等常见写法并借助效果图与说明文档快速复现作业样式在模仿中熟悉HTML标签结构与页面排版思路为后续表单控件和简单布局练习打下基础。1. 十分钟的活干了两小时这份 HTML 表格源码卡在哪个环节做后台页面的人大概都遇到过需求文档上写着「加一个列表」你估十分钟结果调了一下午——表头跟内容错位半格窄屏上表格把整页撑出横向滚动条复制到 Excel 里表头丢了给某一列加了固定宽度后长文本又挤成竖排汉字。压缩包html制作简单的表格源码.zip解决的就是最前面那段重复劳动它给出的是一份结构完整、能直接双击在浏览器里看到效果的 HTML 表格源码table、thead、tbody、th、td该有的层级一个不少字段换成自己的就能用。适合三类人刚学 HTML、需要一份能对照的正确写法的新手要快速搭后台列表页、不想每次都从空白文件敲标签的前端以及做数据展示页、需要一份可交付静态原型的从业者。它不承担业务逻辑也不带任何框架和构建工具纯静态结构——这点得先说清楚否则你会指望它顺手把排序和分页也做了。2. 拆骨架table/thead/tbody 的四层语义与 colspan 网格算术先别急着改颜色。表格出问题十次里有七次不是 CSS 的锅而是结构写歪了少一个td、rowspan占位没扣、表头用了td加粗冒充。这一章把源码里那套骨架逐层拆开讲清楚每一层为什么必须存在以及合并单元格时那张看不见的网格该怎么算。2.1 最小可用骨架四层结构缺一层都会在别处还债一份能长期维护的表格结构上至少分成四块caption说这张表是什么thead放列标题tbody放数据行tfoot放合计或备注。浏览器对残缺结构很宽容你不写tbody它会自动补一个但自动补出来的东西你在调试时看不见用脚本按table.rows取值时下标就会对不上。所以源码里显式写全是给后面所有操作打底。!DOCTYPE html html langzh-CN head meta charsetutf-8 !-- 窄屏适配的第一件事不是 CSS是这个 viewport -- meta nameviewport contentwidthdevice-width, initial-scale1 title设备巡检记录/title /head body table classdata-table !-- caption 必须是 table 的第一个子元素位置错了会被浏览器挪走 -- caption第三季度设备巡检记录/caption thead tr th scopecol序号/th th scopecol设备编号/th th scopecol巡检日期/th th scopecol状态/th /tr /thead tbody tr td1/td tdDEV-0142/td td2024-07-03/td td正常/td /tr tr td2/td tdDEV-0157/td td2024-07-05/td td待复检/td /tr /tbody tfoot tr !-- 合计行跨前三列colspan 之和要跟表头列数对齐 -- td colspan3合计/td td2 台/td /tr /tfoot /table /body /html这段代码可以直接存成.html打开看。几个参数值得单独说langzh-CN决定读屏软件用中文语音朗读也影响localeCompare之类的排序行为scopecol是给辅助技术看的告诉它「这个 th 管的是下面这一整列」tfoot写在tbody后面是常见写法规范上也允许放在thead之后但放前面会让部分老版本浏览器的打印分页变得奇怪我一般统一放最后。caption常被当成可有可无的装饰实际上它是表格唯一的「名字」。屏幕阅读器进入表格时会先念 caption再念列名。没有它用户听到的是一串孤立的数字。样式上如果不想让它占一行可以用caption-side: bottom挪到下面或者视觉隐藏但保留在无障碍树里——千万别用display: none那样读屏软件也读不到了。2.2 th 与 scope表头不是「加粗的单元格」新手最常见的写法是把表头写成tdb设备编号/b/td视觉上看不出区别代价是整张表在无障碍层面失去了列名。用th之后浏览器和读屏软件都知道这是表头scope再把「管哪一列、管哪一行」说清楚。scope 取值用在哪实际含义colthead里的th该表头管理它所在的那一列row数据行首列的th该表头管理它所在的那一行colgroup与colgroup分组配合管理一组列多级表头里用rowgroup与tbody分组配合管理一组行较少用scope不是必须写的浏览器会做一定程度的推断但推断规则在复杂表格里并不靠谱尤其是存在合并单元格时。行列交叉的表格第一列是行标题、第一行是列标题最容易被读错这种结构下首列要用th scoperow交叉单元格本身则用td。如果表格复杂到scope表达不了——比如多级表头盖住好几列且分组不规则——就得改用idheaders的方案给每个表头单元格一个id数据单元格用headersa b列出它对应的表头 id。这套写法维护成本高一般只有在财务报表、课表这类结构时才会动用到源码里默认不带需要时再补。2.3 colspan 与 rowspan先在心里画网格再写属性合并单元格是表格错位的头号来源原因几乎都是同一个写的时候盯着「这一行有几个格子」而不是「这一行在网格上还剩几个格子」。rowspan一旦用上被它占掉的位置在后续行里就不能再写td了。举个例子三列的表第一列是分组名一个分组下挂两条记录tbody !-- 第一行占用第 1 列rowspan2后两列各占一格共 3 格 -- tr th scoperowgroup rowspan2A 组/th td甲设备/td td3/td /tr !-- 第二行第 1 列已被上一行的 rowspan 占掉所以只能写 2 个 td -- tr td乙设备/td td5/td /tr /tbody校验方法很简单写在纸上或者注释里都行每一行的「td 数量 各 td 的 colspan 之和 被上方 rowspan 占用的列数」必须等于总列数。三条加起来对不上页面就会整行右移而且浏览器不会报错只会默默把多出来的格子挤到下一行。我在改别人源码时养成的习惯是先数thead有几个th再把tbody每一行按上面这个式子加一遍比在浏览器里肉眼找错位快得多。还有一个容易忽略的点rowspan跨行之后斑马纹和:nth-child的视觉节奏会断掉被合并的那几行看起来像一整块。这种情况下要么放弃斑马纹改用border-bottom分隔要么靠border-collapse的边框来划分区域别硬撑。3. 样式参数border-collapse、斑马纹与 sticky 表头的四组取舍结构对了剩下的是参数。表格的 CSS 有几个「看似可选、实际必选」的属性不设border-collapse你的边框就是双线不设table-layout你的列宽就会随内容抖动不把position: sticky写在正确的元素上表头就吸不住。这一章按这四个点逐个给参数和取舍理由。3.1 border-collapse 与 border-spacing两套边框模型不能混着用表格有两套边框模型。默认是separate每个单元格各画各的框相邻两条边框中间还有border-spacing的间隙collapse则把相邻边框合并成一条。绝大多数数据列表要的是后者因为separate下你会得到 2px 的双线视觉上格外粗糙。table.data-table { width: 100%; border-collapse: collapse; /* 相邻单元格共享边框双线变单线 */ border-spacing: 0; /* collapse 下此属性失效写上只为将来切模型时不残留间隙 */ table-layout: fixed; /* 列宽由首行/colgroup 决定长内容不再撑宽整张表 */ font-size: 14px; } /* 只画一条底线外框交给 table 自己避免四边边框粗细不一致 */ table.data-table th, table.data-table td { padding: 10px 12px; border-bottom: 1px solid #e5e6eb; text-align: left; /* th 默认居中统一成左对齐更符合列表阅读习惯 */ vertical-align: middle; overflow-wrap: anywhere; /* 长编号、长 URL 强制换行防止撑破列宽 */ } table.data-table { border: 1px solid #e5e6eb; }参数说明border-collapse: collapse写在table上不是写在td上——写在td上无效这是很常见的翻车点。合并之后td之间相邻的边框会由浏览器决定保留哪一条规则大致是「宽者胜、样式优先者胜、颜色深者胜」所以相邻两个单元格最好只让一个设边框两边都设容易出现某一段边框颜色跟预期不符。border-spacing只在separate模型下生效写在这里是防御性的。overflow-wrap: anywhere是我在表格里最喜欢的一个属性。数据里一旦出现长设备编号、长 URL、没有空格的一长串字符table-layout: auto会把整列撑到夸张的宽度fixed下则会溢出到相邻单元格。加上它之后长串会在任意位置折行列宽保持不变。3.2 斑马纹与 hover:nth-child 计数的是「兄弟」不是「数据」斑马纹的常规写法是tbody tr:nth-child(even)。这里有个前提容易被忽略:nth-child数的是同一父元素下的所有兄弟元素只要tbody里混进了非数据行奇偶就会整体错位。哪些东西会混进来空数据时的「暂无数据」占位行、模板渲染留在 DOM 里的tr hidden、分组小标题行。这三种在真实项目里都出现过。对策是给数据行加类名把选择器限定在真正的数据行上/* 有数据行的类名就用类名没有就退回到 nth-of-type */ tbody tr.data-row:nth-child(even) { background: #fafbfc; } /* hover 提权到 td 上避免被单元格的内联背景色覆盖 */ tbody tr.data-row:hover td { background: #eef4ff; }nth-child和nth-of-type的区别值得记一下nth-of-type按标签名筛选后再计数所以在tbody里混了tr之外的元素比如某些模板引擎塞的注释节点不算但塞了template会算时更稳但如果你混进来的本来就是tr两者是一样的都得靠类名。结论数据行一定加类名选择器不要裸奔。hover 的背景色写在第tr上还是td上也有差别写在tr上时如果某个td自己有背景色比如状态列染色会把整行 hover 效果切断一截写在td上则能统一覆盖。这是个细节但做出来的成品观感差别很明显。3.3 table-layout: fixed什么时候必须锁死列宽table-layout: auto是默认值浏览器按内容分配列宽。后果是数据一变列宽就跳加载第二页时表格从 800px 突然变成 1200px用户眼睛要重新找列。这种「跳一下」的问题在后台系统里尤其明显因为那里的数据是异步刷新的。fixed模式下列宽由第一行的单元格或者colgroup决定跟内容多少无关渲染也更快——浏览器不需要先测量所有内容再排版。代价是内容不得不适应列宽所以通常要配合colgroup或给th指定宽度table classdata-table !-- 按列声明宽度顺序与表头一致 -- colgroup col stylewidth: 64px !-- 序号窄 -- col stylewidth: 160px !-- 设备编号固定 -- col !-- 说明吃掉剩余空间 -- col stylewidth: 96px !-- 状态 -- /colgroup thead.../thead tbody.../tbody /table注意两点。第一fixed要生效table本身必须有确定的宽度width: 100%或具体像素值没写宽度浏览器会退回auto。第二未指定宽度的列平分剩余空间所以顺序很重要——把最需要宽的那一列留空剩下的自然被挤窄。如果某一列必须单行显示不许换行比如编号用三件套white-space: nowrapoverflow: hiddentext-overflow: ellipsis三个缺一个都出不来省略号。这时还得给单元格加title属性让用户能悬停看全文否则被截断的内容就彻底看不到了。3.4 sticky 表头写对元素、链路干净、背景不透明长表格滚动时表头吸顶是这份源码里最实用的一段样式也是最容易「写了没反应」的一段。三个条件缺一不可。第一position: sticky要写在th上不要写在thead上。thead的 sticky 行为在部分渲染引擎里不生效而th是稳定的。第二从th到最近的滚动祖先之间不能有overflow: hidden。这一条最坑很多布局组件为了裁剪圆角会在卡片容器上写overflow: hidden表头的 sticky 立刻失效而且控制台不会有任何提示。排查方法是从th逐层往上翻计算样式看overflow是不是visible。同样祖先上的transform、filter、will-change会创建新的包含块让 sticky 相对它定位表现就是「吸在了错误的位置」。第三必须有背景色和不透明层级thead th { position: sticky; top: 0; z-index: 2; /* 要盖住数据行但别大过弹层 */ background: #f7f8fa; /* 不写就会透出下面滚动的文字 */ box-shadow: inset 0 -1px 0 #e5e6eb; /* sticky 后 border-bottom 会被滚走用内阴影补齐分割线 */ }box-shadow: inset这一手是为了补边框。在border-collapse: collapse模型下th的下边框属于表格边框的一部分元素吸顶后那条边框并不跟着走视觉上表头和数据之间就少了一条线。用内阴影模拟一条底线效果稳定。4. 窄屏适配横向滚动、卡片化重排、隐藏列三种方案的适用边界表格天生不适合窄屏列多、字密、必须对齐。源码默认给的是宽屏布局拿到移动端会直接把页面撑开。三种主流方案各有用武之地选错的代价是用户得多点两下才能看到关键信息。这一章把三种方案的代码、适用条件和失败模式都摆出来。4.1 横向滚动改动最小但要求列宽有下限最省事的做法是套一层滚动容器让表格自己横向滚页面不跟着滚。div classtable-wrap table classdata-table.../table /div.table-wrap { width: 100%; overflow-x: auto; /* 只在这里滚页面主体不动 */ -webkit-overflow-scrolling: touch; /* 移动端惯性滚动老写法保留无副作用 */ } .table-wrap .data-table { min-width: 720px; /* 小于这个宽度就滚而不是把列压扁 */ } /* 关键滚动容器如果是 flex 子项会被内容撑开导致滚不动必须归零 min-width */ .flex-parent .table-wrap { min-width: 0; }参数就一个核心min-width。它决定了「什么时候开始滚」。设太小列被压得每个字一行设太大用户在手机上要横滑半天。我的经验值是列数 × 90px 左右再加上固定列的实际宽度宁可滚也不要压缩。这套方案最适合后台管理系统——那些页面本来就在桌面用移动端只是兜底。失败模式有两种容器在 flex 或 grid 布局里没有min-width: 0表格把容器撑到跟内容一样宽滚动条压根不出现或者表格自身没有min-width于是列宽被压到极限内容全部竖排比滚动还难读。4.2 卡片化重排数据要能一行一条地读面向手机用户的展示页——订单列表、成绩单、行程详情——横向滚动体验很差用户看不清当前在哪一列。这时用媒体查询把表格「拆」成一条条卡片media (max-width: 640px) { /* 表头视觉隐藏但保留在无障碍树里不能用 display: none */ .data-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } .data-table, .data-table tbody, .data-table tr, .data-table td { display: block; width: 100%; } .data-table tr { border: 1px solid #e5e6eb; border-radius: 6px; margin-bottom: 12px; padding: 4px 0; } .data-table td { display: flex; justify-content: space-between; gap: 16px; border-bottom: 1px dashed #eef0f3; text-align: right; } /* 列名从>tr classdata-row td>/* 推荐用语义类名控制而不是 nth-child 数序号 */ media (max-width: 480px) { .data-table .col-secondary, .data-table td.col-secondary { display: none; } }不要用nth-child定位要隐藏的列。原因很实在只要以后有人加了一列所有序号往后挪一位被隐藏的列就全错了而且这种错误只在窄屏出现桌面端测不出来。用类名改动时跟着列一起动。另外要意识到display: none的副作用读屏软件也读不到这列内容某些浏览器里连复制粘贴都取不到。如果被隐藏的列可能包含关键信息比如备注里写了异常原因就必须补一个「展开全部」的开关把类名切掉。这是产品层面的取舍不是纯样式问题值得在交付时明确说明。5. 五个翻车现场从表头丢失到 innerHTML 拼串的排查记录下面这五条是我改表格源码时反复遇到的。每条按「现象 → 原因 → 解决」写照着对号入座基本能定位。5.1 结构与表头类问题第一条整行右移一格最后一列数据消失。现象是表格从某一行开始整体往右挪最后一列被挤到表格外或者直接看不见。原因几乎都是某行的单元格数量写多了多数情况下是上方有rowspan占位写的时候忘了那一列已经被占掉多写了一个td。解决方法是按 2.3 的公式数一遍每行「td 数 colspan 之和 上方 rowspan 占用列数 总列数」。在开发者工具里给td加临时描边outline: 1px solid red能一眼看出哪一行多出一格。第二条滚动后表头文字消失只剩空白条。现象是表头吸顶了位置对但里面的文字或背景不见了。原因通常是表头没写成th而是用td加font-weight: bold冒充而 sticky 样式是针对th写的自然不生效另一种是把position: sticky写在了thead上部分渲染引擎不认。解决是把表头统一改回thead tr thsticky 规则落在th上并检查祖先链路上有没有overflow: hidden。5.2 样式与视觉类问题第三条边框变成双线1px 看着像 2px。现象是每个单元格四周都有边框相邻两条并排整体显得笨重。原因是table上没有写border-collapse: collapse浏览器用了默认的separate模型。解决是在table上补border-collapse: collapse同时把边框收敛到一侧——只给td留border-bottom外框交给table。另外要注意在collapse模型下border-spacing无效如果代码里两个都写了别以为是border-spacing救了场。第四条斑马纹隔行错乱某几行颜色连着。现象是本来该交替的底色在某个位置断掉出现连续两行同色或三行同色。原因有两个tbody里混进了非数据行「暂无数据」占位行、tr hidden的模板行nth-child把它们也算进了计数或者数据是异步追加的追加时没重排索引导致奇偶基准变了。解决是给真实数据行加>// 推荐用 DOM API 建行内容一律走 textContent天然免疫标签注入 function appendRow(tbody, values) { const tr document.createElement(tr); tr.className data-row; values.forEach((v, i) { const td document.createElement(td); td.textContent String(v ?? ); // 空值统一成空串避免出现 null if (i 0) td.dataset.label 设备编号; // 供卡片化布局取标签 tr.appendChild(td); }); tbody.appendChild(tr); }这段代码的关键就一句textContent不会解析 HTML。代价是不能再塞图标标签需要图标时单独createElement一个span放进去比拼接字符串麻烦一点但省掉了后面所有的转义烦恼。提示改完表格后用开发者工具的「无障碍」面板看一眼结构树。如果th的scope显示为「列」、caption出现在表格名称位置说明骨架是对的如果表头显示成普通单元格说明你漏了th。6. 补一个排序函数再交付上线前的三项验证动作静态表格能看不能用最常见的追加需求是点表头排序。不加库的情况下二十行代码足够核心在于排序后的行要「移动」而不是「重建」——appendChild一个已经存在的节点会把它从原位置摘下来正好用来原地重排事件监听也不会丢。// 给表头绑定点击排序单列排序反复点击切换升降序 function enableSort(table) { const tbody table.tBodies[0]; // 只取数据行占位行和模板行不参与排序 const getRows () Array.from(tbody.querySelectorAll(tr.data-row)); table.querySelectorAll(thead th).forEach((th, index) { let asc true; th.style.cursor pointer; th.addEventListener(click, () { const rows getRows().sort((a, b) { const x a.cells[index]?.textContent.trim() ?? ; const y b.cells[index]?.textContent.trim() ?? ; const nx Number(x), ny Number(y); // 两边都是有效数字且非空才按数值比否则按中文本地化字符串比 const cmp (x ! y ! !Number.isNaN(nx) !Number.isNaN(ny)) ? nx - ny : x.localeCompare(y, zh-Hans-CN); return asc ? cmp : -cmp; }); rows.forEach(r tbody.appendChild(r)); // 已有节点会被移动不会克隆 asc !asc; th.dataset.order asc ? asc : desc; // 留给箭头图标用 }); }); }参数和逻辑说明a.cells[index]按列下标取单元格所以表头顺序必须和数据列严格一致这也是前面强调结构的原因数值判断特意排除了空串因为Number()是 0会让空单元格排到最前面localeCompare带zh-Hans-CN参数中文按拼音序而不是码点序否则「张」会排到「李」前面。日期列如果是2024-07-03这种格式字符串比较恰好等价于时间比较不用额外转换如果是2024/7/3这种不补零的写法就必须先new Date()再比这是我在一个项目里踩过的坑——七月的数据排到了十二月后面。交付前我会固定做三件事。第一把窗口拉到 320px 宽看有没有横向溢出判断标准不是「能不能滚」而是「页面主体会不会跟着滚」后者说明有个元素超宽了。第二全选表格复制粘到表格软件里看列是否还对齐——用户对表格最真实的高频操作就是复制走colspan写错在这一步暴露得最彻底。第三用键盘 Tab 键走一遍确认可点击的表头能获得焦点必要时给th加tabindex0并监听回车只用鼠标测是测不出来的。从那以后我每次拿到一份表格源码都会先数清thead的列数、按公式核对每一行的格子、再把窄屏和复制粘贴这两步走一遍最后才动样式。顺序反了后面全是返工。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?