表单和表格这两个东西,在HTML里看着是最"老实"的一对,但你真去写业务页面的时候会发现,它们恰恰是最容易翻车的地方。我见过不少新人,CSS动画玩得飞起,一让他做个报名表单或者数据列表页,样式就开始走样——input对不齐、select箭头长得各不一样、表格一多几列就挤成一团。我印象最深的一次是帮人排查一个订单管理页面,表格在最外层一个overflow容器里,结果表头不固定,列宽被内容撑得乱七八糟,IE上一打开直接错位。后来我发现,问题不在框架,而是基础没打牢:HTML结构不语义化,CSS又只写了"看得过去"的样式,根本没考虑极端情况。
所以这篇东西,我想把HTML表单和表格这两块,从结构到样式,再到移动端兼容,做一个完整但务实的拆解。你不需要会什么框架,只要HTML和CSS基础就行,目标是让你看完能直接上手写一个像样的、能扛住真实业务场景的表单和表格页面。这篇文章不追新,都是生产环境里反复用到的东西。
1. 表单与表格:为什么它们能单独开一课
1.1 被低估的复杂度
很多初学者有个错觉:表单不就是一堆input套在一个form里,表格不就是table套tr套td吗?对,字面上确实是这样。但一旦进入真实场景,你会遇到一堆问题:栅格布局里两个输入框怎么保持等宽?label和input怎么垂直居中对齐且点击文字能聚焦?textarea的拉伸图标要不要保留?表格的列宽怎么定才能让内容不换行?整行hover高亮怎么做才不影响单元格背景?
这些问题叠加在一起,复杂度就上来了。我在面试前端岗位的时候,特别喜欢让人手写一个表单和表格,不涉及任何复杂的JS,就只是HTML和CSS。为什么?因为这两样东西能反映一个人对语义化、布局原理、浏览器默认样式、可访问性有没有系统理解。能快速写出一套干净结构的人,写业务页面基本不会写出灾难代码。
1.2 官方标题的"02"在暗示什么
这个系列的命名是"HTML CSS 02 表单与表格",言下之意是你在学完基础标签和选择器之后,需要进入一个"能用HTML和CSS完成实际界面模块"的阶段。表单和表格就是很好的切入点:它们足够独立、足够常用、又足够能检验你前面学的CSS功底。我建议你把这一课当成一个"阶段验收",学完以后试着完整实现一个带校验提示、斑马纹、表头固定的页面,能搞定,说明你前期的HTML/CSS基础是真扎实了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 表单的HTML骨架:先搞清楚form里到底该放什么
2.1 form标签的职责边界
很多新手把form当成一个普通的容器div来用,其实form是有明确职责的:它定义了数据提交的范围、方式和目标地址。最关键的三个属性是action、method、name,还有一个很容易被忽略的id。action是数据发往的URL,method是GET还是POST,name是表单的名字,id主要用于JS关联。真正需要在页面上提交数据的场景,form是必须的,但如果你的页面只是纯前端交互、用JS读用户输入,那可以考虑不上form,避免按回车时莫名触发表单提交。
实际开发中有一个经常踩的坑:form内嵌一个button,忘了写type="button",结果点击按钮时页面刷了一下。这是因为button的type默认值是submit,在form里按回车或点击会自动提交。所以我的习惯是:凡是不负责提交数据的按钮,一律显式写type="button"。
2.2 常用控件:input的类型选择
input是表单里最核心的标签,但它的行为完全由type决定。我需要重点说几个高频类型:
- text:单行文本,业务里最常用
- password:密码输入,会自动打码
- email、url、number、tel:移动端会调起对应键盘,PC端可配合校验
- date、time:日期时间选择,PC端浏览器的原生样式还可以
- checkbox、radio:多选和单选
- file:文件上传
- hidden:隐藏字段,经常用来传一些不需要用户看到的业务数据
我特别建议你注意type="email"和type="tel"这类语义化类型。它们的作用不只是校验,还有移动端键盘类型的变化。比如给一个手机号输入框设置type="tel",在手机上会调出数字键盘,这对用户体验的提升非常明显。
2.3 label与表单控件的三种关联方式
label是表单里最能体现专业度的标签之一。它最核心的作用是:点击label文字时,对应的输入框自动聚焦。这看起来是个小事,但对键盘用户和鼠标用户来说都极其重要——鼠标用户不用精确点击小输入框,键盘用户用辅助技术时能正确读到字段名。
关联方式有三种:
- 用for属性指向input的id,这是最推荐的方式,比如
<label for="username">用户名</label><input id="username" type="text"> - 把input直接嵌套在label里面,这种做法可以不用id,但结构上比较局限
- 用aria-label属性做无障碍标注,适合那种视觉上不显示label的搜索框
顺便说一个最容易犯的错:label里的for值必须和input的id严格一致,大小写也要一致。我之前排查过一个问题,页面上怎么点文字都没反应,查了半天,发现for写成了"user-name",id写的是"username",就一个连字符的差别。
2.4 结构化分组:fieldset与legend
当表单字段很多的时候,光靠一堆label和input堆在一起,视觉上会显得特别乱。这时候需要fieldset和legend出场。fieldset把一组相关字段圈在一起,legend则是这组的标题。比如一个注册表单,可以分成"账号信息"和"个人资料"两个fieldset,每组一个legend,这样用户一看就明白这个区块是干嘛的。
浏览器对fieldset有默认的边框样式,不一定好看,但你可以在CSS里重置掉。不过这个语义化结构本身是有价值的,特别是在长表单里,对屏幕阅读器的友好度提升非常明显。
3. 表格的HTML结构:thead、tbody、tfoot一个都不能少
3.1 表格的语义构成
表格远比表单容易被轻视。很多人写表格就是table>tr>td,看起来能显示数据,但距离规范的结构差远了。一个规范的表格结构应该是:
table
- thead:表头区域,里面放表头行tr和表头单元格th
- tbody:数据区域,放数据行和td
- tfoot:表尾区域,放合计、备注之类
为什么要这样分?除了语义清晰以外,还有两个实际好处。第一,CSS可以精确控制区域样式,比如给thead里的th加背景色和sticky定位,给tbody加斑马纹;第二,渲染可以分批加载,浏览器在拿到tbody数据时就可以开始渲染,而不必等整张表加载完。
3.2 th不只是加粗的文字
th是表头单元格,默认是加粗居中,但它的价值绝对不只是加粗。th上有个scope属性,可以声明这个表头是行头还是列头。scope="col"表示这一列的表头,scope="row"表示这一行的表头。这个属性对无障碍支持很重要,屏幕阅读器读表格时能够正确关联表头和单元格。
我在实际项目中见到很多人只用td装表头,用CSS去加粗。能看,但语义上是不对的。如果你希望表格能被搜索引擎、辅助工具正确理解,表头就该用th。
3.3 合并单元格:rowspan与colspan的正确理解
合并单元格是表格里最容易被搞错的功能。colspan表示一个单元格横跨几列,rowspan表示一个单元格横跨几行。我建议你写合并之前,先在草稿纸上画一个网格图,标清楚哪个位置要跨行、哪个位置要跨列,这样写代码时才不会数错列数。
有一个容易踩的坑是:用了colspan之后,同一行其他单元格的数量就会减少,你不需要为被合并掉的列再补空td。很多人合并后出现表格错位,就是因为多了或者少了单元格。写完合并表格,建议在浏览器里数一数每行的实际列数是否一致,这是最快的排查方式。
3.4 caption:给表格一个完整标题
caption是表格的标题,放在table标签内部、表头之前,即使不显示,对语义化也有帮助。我习惯在每个数据表格里都加上caption,哪怕视觉上把它做成一个隐藏的标题。这样既能让屏幕阅读器读出表格的主题,也能让代码结构更完整。
4. 用CSS让原生控件告别"敷衍的默认外观"
4.1 先统一表单控件的基础尺寸
浏览器给表单控件的默认样式,在不同系统上差异非常大。Windows的按钮是经典直角灰底,macOS的按钮是圆角蓝底,input的内边距也各不相同。所以写CSS的第一步,不是急着美化,而是先"归零"。
我常用的做法是,给input、select、textarea统一设置:
css复制input,
select,
textarea {
font: inherit;
font-size: 16px;
box-sizing: border-box;
margin: 0;
}
font: inherit很重要,因为表单控件默认不继承父元素的字体,如果你body里设了特定字体,input里还是用默认字体,看起来会特别突兀。font-size设成16px是为了防止移动端iOS在输入时自动放大页面(iOS对font-size小于16px的输入框会触发自动聚焦缩放,这个后面细说)。box-sizing: border-box能保证你设置的width就是实际占用的宽度,不会被padding撑大,这是布局稳定的前提。
4.2 让input和select的聚焦状态"有反馈"
用户点击输入框时,输入框应该给他一个明确的聚焦反馈。浏览器默认会给input一个蓝色的outline,但很多团队觉得丑,就粗暴地outline: none。这是最让我头疼的做法,因为一旦去掉outline又没加替代样式,键盘用户根本不知道焦点在哪,可访问性直接崩掉。
更好的方案是尊重outline但微调它:
css复制input:focus,
select:focus,
textarea:focus {
outline: 2px solid #4a90e2;
outline-offset: 1px;
border-color: #4a90e2;
}
这样既保留了焦点可见性,又和整体设计风格统一。outline-offset可以让光晕和边框之间留一点间距,看起来更精致。
4.3 select的下拉箭头自定义
select元素在不同浏览器里长得完全不一样,尤其是右侧那个下拉箭头,几乎无法直接控制。如果想让它跟页面风格统一,最常用的办法是把select的appearance设为none,去掉原生箭头,然后自己用背景图或者伪元素画一个。
css复制select {
appearance: none;
-webkit-appearance: none;
background: url("data:image/svg+xml,...") no-repeat right 12px center;
padding-right: 36px;
}
这里的背景图可以用一个简单的SVG箭头。注意padding-right要留够,否则文本会盖住箭头。如果你用背景图,记得把背景图的颜色和整体配色配合好。
4.4 checkbox和radio的自定义样式
checkbox和radio在默认状态下很难融入好看的UI。我常用的做法是:先把input本体隐藏(但不能display: none,因为那样会丢掉可访问性),然后借助label和伪元素绘制一个自定义的图标。核心逻辑是,通过相邻兄弟选择器,让label在checkbox被选中后切换状态。
HTML结构大概是这样:
html复制<label class="checkbox-item">
<input type="checkbox" class="checkbox-input">
<span class="checkbox-box"></span>
我同意用户协议
</label>
CSS的核心部分:
css复制.checkbox-input {
position: absolute;
opacity: 0;
}
.checkbox-box {
width: 18px;
height: 18px;
border: 2px solid #ccc;
border-radius: 4px;
display: inline-block;
margin-right: 8px;
}
.checkbox-input:checked + .checkbox-box {
background: #4a90e2;
border-color: #4a90e2;
}
.checkbox-input:checked + .checkbox-box::after {
content: "";
...
}
这里有个细节:隐藏input时最好用position: absolute + opacity: 0,而不是display: none,这样键盘用户Tab键依然能聚焦到它,配合焦点样式也能做出键盘反馈。
4.5 表格的经典样式三件套
表格的样式没有表单那么碎,但有几个关键点。
第一是边框合并。table默认的border-collapse是separate,单元格之间有缝隙,边框会变成双线。想让它变成干净的网格线,几乎99%的场景都要设:
css复制table {
border-collapse: collapse;
}
第二是内边距。th和td一定要给足padding,否则内容贴边很难看。我常用的是12px 16px。
第三是斑马纹和hover高亮。斑马纹用nth-child(even)就能做,hover高亮要注意优先级:
css复制tbody tr:nth-child(even) {
background: #f8f9fa;
}
tbody tr:hover {
background: #e9f0ff;
}
这里有个坑:如果某个单元格td自己有背景色,hover时tr的背景会被td盖住。解决方法是改成给td设透明背景,或者在hover的时候统一给td加背景色。否则你会看到hover时只有单元格间隙变色,整行高亮的效果出不来。
5. 实战:一个报名表单加数据表格的完整实现
5.1 需求拆解与结构设计
纸上谈兵够了,我们来做一个具体的页面。它包含两部分:上半部分是报名表单,下半部分是用表格展示已报名人员。这个场景在真实业务里非常常见——后台管报名、前台填资料。
表单需要这几个字段:姓名(文本输入)、手机号(数字键盘)、邮箱(邮箱校验)、报名课程(下拉选择)、是否接受调剂(radio)、个人简介(多行文本)、提交和重置按钮。
表格需要展示:序号、姓名、手机号、课程、报名时间、状态。
这里有一个交互细节值得注意:表单提交后,理论上表格里应该多一行数据。但这一课是纯HTML/CSS,不做数据库交互,所以我的做法是在表格tfoot里给一行示例数据,用来演示表格样式。如果你学的后面接了JS,再把这两部分联动起来。
5.2 表单部分的实现
HTML结构我建议按fieldset分组,这样语义清晰。字段用label + input的结构,ul或者div做布局都行。我用div,每个字段一个form-group:
html复制<form id="applyForm">
<fieldset>
<legend>基本信息</legend>
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" required placeholder="请输入姓名">
</div>
<div class="form-group">
<label for="tel">手机号</label>
<input type="tel" id="tel" name="tel" pattern="1[3-9]\d{9}" required placeholder="11位手机号">
</div>
...
</fieldset>
<fieldset>
<legend>报名信息</legend>
...
</fieldset>
<div class="form-actions">
<button type="submit">提交报名</button>
<button type="reset">清空重填</button>
</div>
</form>
我在input上加了required和pattern。required表示必填,pattern用了正则表达式校验手机号格式。这些属性属于HTML5原生表单校验,不需要写一行JS就能实现基础校验。这个后面专门展开讲。
还要注意,两个按钮我都明确写了type,submit是提交数据,reset是清空表单。如果你忘了给"清空重填"写reset,它默认就是submit,一点击就提交了,非常尴尬。
5.3 表单布局布局:单列还是多列
表单的布局方式,我建议根据屏幕宽度来定。宽屏上可以让两个短字段并排,比如"姓名"和"手机号"一行放两个,但在窄屏上要自动变成单列。这里推荐用CSS Grid:
css复制.form-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
@media (max-width: 640px) {
.form-row {
grid-template-columns: 1fr;
}
}
grep热词里有人搜"css flex布局子元素宽度自适应",其实在这个场景grid比flex更适合做行列布局,因为grid能精确控制列数,而flex更适合单个方向上的排列。如果你习惯用flex,也可以给两个field直接各设flex: 1,但需要额外的wrap和gap处理,不如grid直观。
5.4 表格的实现与细节
表格部分,我用table + thead + tbody + tfoot的标准结构。表头里放"序号、姓名、手机号、课程、报名时间、状态"六列。状态这一列,我特意做成带背景色的状态标签效果,用span模拟badge。
另外一个很实用的技巧是表头固定:当表格数据很多需要滚动时,让thead吸顶。做法是给th设置position: sticky和top: 0,并确保背景色不透明,否则滚动时内容会透过来:
css复制thead th {
position: sticky;
top: 0;
background: #f1f3f5;
z-index: 1;
}
注意这里的背景色必须是实色,如果用了半透明色,滚动时底下数据透过来会很难看。这就是我在开头说的"表头不固定"问题,其实一行CSS就能解决。
5.5 完整代码示例
我写一个简化但完整的版本,你可以直接复制到编辑器里跑:
html复制<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>报名管理</title>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f7f8fa; margin: 0; padding: 24px; }
.container { max-width: 800px; margin: 0 auto; }
.card { background: #fff; border-radius: 8px; padding: 24px; margin-bottom: 24px; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
legend { font-weight: 600; padding: 0 4px; }
.form-group { margin-bottom: 16px; }
label { display: block; margin-bottom: 6px; color: #333; }
input, select, textarea { width: 100%; padding: 10px 12px; border: 1px solid #d9d9d9; border-radius: 6px; font-size: 16px; font-family: inherit; }
input:focus, select:focus, textarea:focus { outline: 2px solid #4a90e2; outline-offset: 1px; border-color: #4a90e2; }
textarea { resize: vertical; min-height: 80px; }
.form-actions { display: flex; gap: 12px; margin-top: 20px; }
.form-actions button { padding: 10px 24px; border-radius: 6px; font-size: 15px; cursor: pointer; }
button[type="submit"] { background: #4a90e2; border: none; color: #fff; }
button[type="reset"] { background: #fff; border: 1px solid #d9d9d9; color: #333; }
table { width: 100%; border-collapse: collapse; background: #fff; }
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #eee; }
thead th { background: #f8f9fa; position: sticky; top: 0; z-index: 1; }
tbody tr:hover { background: #f5f8ff; }
.status { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 13px; }
.status.pass { background: #e6f7e6; color: #389e0d; }
.status.pending { background: #fff7e6; color: #d48806; }
.table-wrap { overflow-x: auto; border-radius: 6px; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<h2>新增报名</h2>
<form id="applyForm">
<fieldset>
<legend>基本信息</legend>
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" required placeholder="请输入姓名">
</div>
<div class="form-group">
<label for="tel">手机号</label>
<input type="tel" id="tel" name="tel" pattern="1[3-9]\d{9}" required placeholder="11位手机号">
</div>
</fieldset>
<fieldset>
<legend>报名信息</legend>
<div class="form-group">
<label for="course">报名课程</label>
<select id="course" name="course">
<option value="html">HTML与CSS基础</option>
<option value="js">JavaScript入门</option>
<option value="vue">Vue实战</option>
</select>
</div>
</fieldset>
<div class="form-actions">
<button type="submit">提交报名</button>
<button type="reset">清空重填</button>
</div>
</form>
</div>
<div class="card">
<h2>报名列表</h2>
<div class="table-wrap">
<table>
<caption class="table-caption">已报名学员数据</caption>
<thead>
<tr>
<th>序号</th>
<th>姓名</th>
<th>手机号</th>
<th>课程</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr><td>1</td><td>张三</td><td>13800138000</td><td>HTML与CSS基础</td><td><span class="status pass">已确认</span></td></tr>
<tr><td>2</td><td>李四</td><td>13900139000</td><td>JavaScript入门</td><td><span class="status pending">待确认</span></td></tr>
<tr><td>3</td><td>王五</td><td>13700137000</td><td>Vue实战</td><td><span class="status pass">已确认</span></td></tr>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
这个示例里,我用了一个.table-wrap做包裹容器,配合overflow-x: auto实现表格在小屏幕上的横向滚动。这样即使列再多,也不会把页面撑破。
6. 移动端与浏览器兼容:进阶的必修课
6.1 iOS的输入框聚焦缩放问题
这是移动端表单最经典的问题。在iOS Safari里,如果input的font-size小于16px,点击输入时页面会自动放大。用户刚聚焦就被强制放大,体验非常撕裂。
解决方案很简单:给input、select、textarea都设置font-size: 16px或更大。所以我在前面统一样式时特意写font-size: 16px。如果你设计的字体很小,可以给input单独设置16px,再通过transform: scale去视觉上缩小,但这就绕了。最稳妥的就是设置16px,虽然看起来稍微大一点,但对移动端用户来说是舒适的。
6.2 苹果底部安全区与表单按钮
热词里有一条"小程序苹果底部兼容css",其实在普通网页里也存在这个问题:iPhone的底部横条会遮挡固定在底部的按钮。解决方式是用safe-area-inset-bottom这个环境变量:
css复制.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-bottom: env(safe-area-inset-bottom);
}
如果你的页面有固定在底部的"提交"按钮,建议都要加上这个padding,否则在iPhone上按钮文字可能被底部横条盖住。视觉稿上看不出来,真机一测就翻车。
6.3 表格在小屏上的自救方案
表格在PC上是横着展示的,但在手机屏上,如果列太多,就只有一个下场:溢出去。我的处理思路分两级:
第一级,外层包一个overflow-x: auto容器,让表格在小屏上可以左右滑动。这保留了表格原有的行列结构,代价是用户需要左右拖动,但至少页面布局不会崩。
第二级,如果表格列数少、语义简单,可以尝试把每行变成卡片式布局。做法是用CSS把td变成块级,加上data-label属性来显示字段名:
css复制@media (max-width: 640px) {
thead { display: none; }
tr { display: block; margin-bottom: 16px; }
td { display: block; text-align: right; }
td::before { content: attr(data-label); float: left; }
}
这种方式实现起来快,但要注意:data-label这个属性需要你在HTML里为每个td手动添加,比如<td data-label="姓名">张三</td>。而且屏幕阅读器对这种布局的解读可能不理想,所以我一般只推荐在管理后台这类内部场景用。
6.4 select下拉选项在安卓和iOS上的差异
select在移动端的行为差异很大。iOS上点击select,通常会弹出滚轮选择器,选项的样式基本由系统控制,你改不了。安卓上不同内核也有差别。所以如果你对select的美化有很高的要求,又必须兼容移动端,我的建议是别硬杠原生select,直接用一个模拟下拉组件(比如自制的下拉面板或者成熟的UI库组件)。但如果你只是做一个内部后台页面,原生select完全够用,省心不少。
7. 前端表单校验:不要把所有希望寄托在后端
7.1 HTML5内置校验属性
表单校验是表单开发里必不可少的一环。很多人以为校验只能靠JavaScript,其实HTML5已经内置了一套完整的校验机制,最常见的属性有:
- required:必填
- minlength 和 maxlength:长度限制
- pattern:正则匹配
- min 和 max:数值范围
- type="email"、type="url" 等类型内置格式校验
用原生属性做校验的好处是零成本、且键盘和辅助设备都能识别。缺点也很明显:每个浏览器对校验失败提示的样式和文案都不一样,而且文案是英文的,很难定制。所以我的做法是:用原生属性做基础校验,然后通过CSS伪类:valid和:invalid来给输入框做视觉反馈。
7.2 用CSS给校验状态加样式
你可以这样写:
css复制input:required:invalid {
border-color: #ff4d4f;
}
input:required:valid {
border-color: #52c41a;
}
但是有一个细节值得注意:如果input是空的且没有required,它会处于:valid状态,所以不加required的字段最好不要用:valid来判定绿色,否则一打开页面都是绿色边框。建议只在required字段上使用。
7.3 JS补充自定义校验逻辑
原生属性能覆盖大部分基础校验,像"两次密码是否一致""手机号是否被注册"这类需要联动或远程判断的逻辑,就必须交给JavaScript。最常见的方式是通过form的submit事件拦截:
javascript复制const form = document.getElementById('applyForm');
form.addEventListener('submit', function (e) {
const name = form.elements.name.value.trim();
const tel = form.elements.tel.value.trim();
if (!name) {
e.preventDefault();
alert('请输入姓名');
return;
}
if (!/^1[3-9]\d{9}$/.test(tel)) {
e.preventDefault();
alert('请输入正确的手机号');
return;
}
});
用form.elements可以直接访问表单里所有带name属性的控件,简单直接。如果你的校验逻辑比较复杂,比如多字段联动,也可以考虑引入校验库,但核心思路不变:校验通过才允许提交,不通过就阻止默认行为。
7.4 重置表单的正确打开方式
热词里有一条"清空表单内容",这里一并说了。最直接的方式是给按钮设置type="reset",一键恢复表单的初始值。但如果你在表单里用JS动态填充过数据,reset并不会清掉这些动态值,它会恢复成HTML里value属性的值。
如果需要彻底清空,可以手动遍历重置:
javascript复制form.reset();
如果你用的是React/Vue这类框架,form.reset()不一定同步更新数据状态,还得把数据模型也重置。所以选方案时先想清楚:你是在原生HTML世界,还是在框架世界里。
8. 一些真实项目里沉淀下来的"经验税"
8.1 样式优先级带来的灵异现象
我在排查表格样式问题时,经常遇到一种情况:我在CSS里给td写了背景色,但页面上没生效。查了很久,发现是框架里其他地方写了table td { ... },优先级比我的类名选择器高。所以给表格单元格写样式时,我会尽量写成table tbody td这类带层级的选择器,提高优先级,减少被污染的概率。
8.2 不要滥用table做布局
这里要提一个老生常谈的禁忌:不要用table做页面布局。十几年前table布局流行过,但现在HTML语义化已经深入人心,页面整体布局应该用flex、grid或更语义化的结构。table只用来展示真正的表格数据。这一点面试的时候经常被问到,实际工作中也是代码评审的红线之一。
8.3 永远留出"可访问性"的退路
最后说一个容易被忽略的点:表单和表格都是可访问性重灾区。没有label的表单,读屏软件读不出来;没有th的表头,读屏软件不知道每列是什么;没有caption的表格,读屏软件不知道表格说的什么内容。这些都不需要花多少成本,但产出的页面质量完全不同。从我多年做前端评审的经验来看,能把表单和表格的可访问性处理好的人,写代码通常都很细心,而细心的工程师做出来的页面,bug率一定低。
这套东西学完之后,建议你找一个真实的页面,比如一个带搜索条件、带数据列表的后台页面,尝试只用HTML和CSS把它完整复刻出来。这个过程会把上面所有知识点串起来,遇到问题时再回头翻对应章节。这样过一遍,后面学框架的表单组件、表格组件时,你会发现自己能一眼看懂组件的源码逻辑,那种感觉还是很爽的。
