CSS 经常被误解成“给网页换颜色的工具”,于是初学者容易陷入两个极端:要么背诵大量属性,却不知道它们为什么没有生效;要么让 AI 一次生成整份样式,只要页面看起来像成品就不再检查。
这篇文章换一种方式学习 CSS。我们会从一条最小规则开始,逐步理解选择器、层叠、继承、盒模型、单位、Flexbox、Grid、响应式和可访问性,再把它们落到 Tracebase 企业知识库工作台中。最后还会复盘三个真实故障,并附上可以直接运行的完整 HTML 与 CSS。
学完以后,你不需要背下所有属性,但应该能够回答这些问题:CSS 如何找到 DOM 元素?浏览器怎样决定最终样式?元素为什么比声明的宽度更宽?Flexbox 和 Grid 应该怎样选择?页面为什么在 768px 下突然难以阅读?AI 生成的 CSS 又该如何验证?
CSS 到底负责什么
HTML 描述内容与结构,CSS 描述这些内容如何呈现。
浏览器读取 HTML 后,会把标签转换成一棵可以在运行时访问的对象树,也就是 DOM。可以把 HTML 文件想成建筑图纸,把 DOM 想成浏览器根据图纸搭好的建筑结构。CSS 不会重新发明房间,而是在现有结构上决定墙面颜色、房间间距、排列方式以及窄屏时如何调整。
一个简化的浏览器处理过程是:
HTML -> DOM
CSS -> CSSOM
DOM + CSSOM -> 渲染树 -> 布局 -> 绘制
CSSOM 可以理解为浏览器整理后的样式规则集合。浏览器会判断每条规则匹配哪些 DOM 节点,计算每个节点的最终样式,再确定尺寸和位置,最后把像素画到屏幕上。
这里有一个重要边界:CSS 可以根据已有状态显示不同外观,但它不负责判断业务状态。
<span class="document-status" data-state="failed">
处理失败:文件编码无法识别
</span>
.document-status[data-state="failed"] {
color: #b42318;
}
HTML 或未来的 JavaScript 决定 data-state 是不是 failed,CSS 只负责把失败状态显示成红色。服务端和数据库仍然负责保存真实的处理结果。
如何把 CSS 连接到 HTML
最常见的工程写法是把样式放在独立文件中,并在 HTML 的 head 中连接:
<link rel="stylesheet" href="styles.css">
三个部分分别表示:
link:当前文档要引用一个外部资源;rel="stylesheet":这个资源与当前页面的关系是样式表;href="styles.css":样式文件的位置。
如果页面完全没有样式,先不要急着修改颜色。应该先在 DevTools 的 Network 面板确认 styles.css 是否成功加载,再检查路径、文件名和浏览器缓存。样式表根本没有连接成功时,后面的规则写得再正确也不会生效。
小型演示也可以使用 style 标签,单个元素还可以使用 style 属性,但真实项目更适合外部样式表。它能把结构与呈现分开,也便于浏览器缓存、团队维护和全局复用。
读懂第一条 CSS 规则
CSS 规则由选择器、属性和值组成:
.document-status {
color: #18794e;
}
.document-status是选择器:在 DOM 中寻找具有这个 class 的元素;color是属性:要修改文字颜色;#18794e是值:使用这一个绿色;- 花括号中的整体叫声明块;
color: #18794e;是一条声明。
一条规则可以包含多个声明:
.documents-panel {
padding: 24px;
background: #ffffff;
border: 1px solid #d8dee6;
}
浏览器不会因为 class 名叫 documents-panel 就自动知道它应该长什么样。class 只是 HTML 与 CSS 之间的稳定接口,真正的视觉结果来自规则中的声明。
注释、空白和书写习惯
CSS 用 /* ... */ 写注释。空格和换行通常不改变规则含义,但统一格式能让层级和错误更容易被发现:
/* 文档处理状态 */
.document-status[data-state="processing"] {
color: #8a6100;
}
属性名拼错、缺少冒号或花括号没有闭合时,浏览器通常不会让整个页面崩溃,而是忽略无法解析的部分。这种“宽容”让页面仍能打开,也让错误更容易被漏掉,因此 DevTools 中的 Styles 面板非常重要。
选择器:CSS 如何找到 DOM 元素
选择器描述匹配条件。常见选择器可以直接映射到 Tracebase:
| 选择器 | 匹配目标 | Tracebase 用途 |
|---|---|---|
a | 所有链接元素 | 设置全局链接颜色 |
.document-status | 具有指定 class 的元素 | 设置所有文档状态的共同样式 |
[data-state="failed"] | 具有指定属性和值的元素 | 找到所有失败状态 |
.document-status[data-state="failed"] | 同时满足 class 与属性条件 | 只设置文档失败状态 |
.product-header a | 页头内部任意层级的链接 | 限定页头链接范围 |
.product-header > a | 页头的直接子链接 | 只选中品牌链接 |
button:hover | 鼠标正悬停的按钮 | 提供交互反馈 |
元素、class 与 id
元素选择器适合设置广泛默认值:
body {
line-height: 1.5;
}
class 可以重复使用,适合组件和可复用样式:
<section class="documents-panel"></section>
<section class="assistant-panel"></section>
id 在一个页面内应该唯一,常用于表单标签关联、标题引用或 JavaScript 定位。它也可以写进 CSS,但 id 选择器优先级很高,组件样式通常更适合使用 class。
<label for="question">你的问题</label>
<input id="question" name="question" type="text">
这里 for="question" 与 id="question" 建立的是可访问性关系,不只是为了 CSS。
属性选择器表达状态
class 更适合说明“这是什么组件”,属性更适合说明“组件现在是什么状态”:
<span class="document-status" data-state="ready">已就绪</span>
<span class="document-status" data-state="processing">处理中</span>
<span class="document-status" data-state="failed">处理失败</span>
.document-status[data-state="ready"] {
color: var(--color-success);
}
.document-status[data-state="processing"] {
color: var(--color-warning);
}
.document-status[data-state="failed"] {
color: var(--color-danger);
}
这会形成清晰接口:业务代码切换属性值,CSS 显示已有状态。不要让 CSS 通过文字内容猜测业务结果。
组合选择器要有边界
选择器越长不代表越专业。过深的选择器容易依赖偶然的 DOM 层级:
body main section ul li span {
color: red;
}
只要中间多包一层元素,这条规则就可能失效。更稳定的写法是直接使用组件接口:
.document-status[data-state="failed"] {
color: var(--color-danger);
}
选择器应该足够精确地限制作用域,但不应把整个 DOM 路径复制进 CSS。
层叠与优先级:为什么我的规则被划掉了
同一个元素可能同时匹配多条规则。CSS 中的“C”代表 Cascading,也就是层叠:浏览器必须为同一个属性选出最终获胜的声明。
可以先记住一个实用的判断顺序:
- 声明是否有效,媒体查询等条件是否成立;
- 来源和重要性是否不同,例如普通作者样式与
!important; - 选择器优先级谁更高;
- 优先级相同时,源码中后出现的规则获胜。
在日常项目中,选择器优先级可以粗略理解为下面四组:
内联样式 | ID | class / 属性 / 伪类 | 元素 / 伪元素
比较时从左向右看,不是简单把所有数量加在一起。
a {
color: var(--color-accent);
}
body .product-header nav a {
color: #b42318;
}
第一条只有一个元素选择器,可以记为 0-0-0-1。第二条包含一个 class 和三个元素,可以记为 0-0-1-3。class 所在的组先获胜,所以导航链接变红,并不只是因为第二条写在后面。
在 DevTools 的 Styles 面板中,失败的 color 会被划掉;Computed 面板则会告诉你最终颜色以及它来自哪条规则。正确修复通常是删除或缩小错误规则,而不是不断添加 !important。后者会把一次局部冲突升级成以后更难覆盖的全局债务。
层叠不等于继承
层叠解决“多条声明争夺同一个元素的同一个属性”;继承解决“子元素没有明确值时,是否沿用父元素的值”。
字体、文字颜色和行高通常会继承:
body {
color: #18212b;
font-family: system-ui, sans-serif;
line-height: 1.5;
}
body 内部的大部分文字会沿用这些值。边框、外边距、内边距、宽高通常不会继承,否则一个面板的边框会自动传给所有后代。
当你看到一个按钮文字颜色发生变化时,要分别检查:
- 按钮是否直接匹配了某条规则;
- 某个可继承值是否来自父元素;
- 浏览器默认样式是否仍在生效;
- 更高优先级的声明是否覆盖了预期规则。
先建立视觉基础:颜色、字体、间距与变量
初学 CSS 时,容易把注意力全部放在布局上。实际上,一个可维护页面还需要稳定的颜色、字体、间距和边框规则。
颜色
CSS 支持十六进制、RGB、HSL 和命名颜色等写法:
color: #18212b;
background: rgb(244 246 248);
border-color: hsl(214 16% 87%);
工程中选择一种主要格式并保持一致即可。颜色不仅要“好看”,还要承担信息职责:正文与背景需要足够对比度,失败、处理中和成功状态不能只靠非常接近的颜色区分。
字体和行高
body {
font-family: Inter, system-ui, sans-serif;
font-size: 16px;
line-height: 1.5;
}
字体栈表示优先使用前面的字体,系统没有时继续回退。正文通常需要比字号更大的行高,避免大段文字挤在一起。标题大小应该符合内容层级,不应为了“显眼”无限放大。
间距、边框和圆角
padding 是内容与边框之间的内边距,margin 是元素与外部其他元素之间的距离。gap 则专门描述 Flexbox 或 Grid 子项之间的间隔。
.documents-panel {
padding: 24px;
border: 1px solid #d8dee6;
border-radius: 6px;
}
面板内部空间用 padding,两个面板之间的距离交给父级 Grid 的 gap,这样职责更清楚。
CSS 自定义属性
同一个品牌色和间距会在多处出现。把设计决策集中到 :root,可以避免搜索替换遗漏:
:root {
--color-canvas: #f4f6f8;
--color-surface: #ffffff;
--color-ink: #18212b;
--color-accent: #1d5fd1;
--space-4: 16px;
--space-5: 24px;
}
a {
color: var(--color-accent);
}
button {
padding: 10px var(--space-4);
background: var(--color-accent);
}
变量定义在 :root 不会让所有元素自动变色。只有主动通过 var(...) 引用变量的声明才会使用它。自定义属性默认可以继承,因此也能在某个组件根节点局部覆盖主题值。
盒模型:声明的宽度为什么不是最终宽度
浏览器把多数元素看成一个由四层组成的盒子:
margin
border
padding
content
默认的 content-box 中,width 只表示内容区。假设输入框是:
.question-form input {
box-sizing: content-box;
width: 520px;
padding: 10px 12px;
border: 1px solid;
}
它的实际占用宽度是:
520 + 12 + 12 + 1 + 1 = 546px
如果它所在的列只有 520px,输入框就会溢出。更符合布局直觉的做法是全局使用 border-box:
*,
*::before,
*::after {
box-sizing: border-box;
}
此时声明的 520px 已经包含左右 padding 和 border,内容区会缩小为 494px,但元素最终占用宽度仍是 520px。
border-box 不能自动解决所有溢出。固定宽度本身仍可能超过手机屏幕,因此表单通常还需要:
.question-form input {
min-width: 0;
width: 100%;
}
常用单位:不要把所有尺寸都写成 px
单位没有绝对的好坏,关键是它表达的约束是否正确。
| 单位或函数 | 含义 | 适合场景 | 常见风险 |
|---|---|---|---|
px | CSS 像素 | 边框、图标、稳定的小尺寸 | 大量固定宽度难以响应 |
% | 相对包含块 | 流式宽度 | 高度百分比常因父级无明确高度而失效 |
rem | 相对根元素字号 | 字号、间距 | 需要统一根字号策略 |
vw/vh | 相对视口 | 全屏区域、少量视口约束 | 移动浏览器工具栏会影响高度 |
fr | Grid 剩余空间份数 | 网格轨道比例 | 只能用于 Grid 轨道 |
min()/max()/clamp() | 在多个尺寸间计算 | 响应式边界 | 表达式过复杂会难维护 |
Tracebase 主区域没有固定为 1180px,而是把 1180px 当作上限:
.workspace {
width: min(calc(100% - 48px), 1180px);
margin-inline: auto;
}
宽屏时最多 1180px,窄屏时则使用视口宽度减去两侧留白。固定上限与流式宽度可以同时存在。
布局前先理解普通文档流
在没有 Flexbox、Grid 或定位时,块级元素通常从上到下排列,行内内容从左到右排布并自动换行。这叫普通文档流。
普通文档流不是“没有布局”,它是浏览器最稳定的默认布局。文章段落、标题和单列移动页面往往不需要额外布局系统。只有当元素之间出现明确的一维或二维关系时,才需要 Flexbox 或 Grid。
不要一开始就给每个元素写 position: absolute。绝对定位会让元素脱离普通流,其他内容无法自然为它让出空间,文本变长或屏幕变窄后尤其容易重叠。
Flexbox:处理一条轴上的排列
Flexbox 适合一维布局:你主要关心一行,或者一列。
Tracebase 页头需要让品牌和导航分居两侧:
.product-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
display: flex让直接子元素成为 Flex 项目;- 主轴默认是水平方向;
justify-content控制主轴分布;align-items控制交叉轴对齐;gap控制项目间距。
导航列表也适合 Flexbox:
.product-header ul {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
flex-wrap: wrap 允许空间不足时换行。没有这条规则,导航可能被压得过窄或冲出屏幕。
常见的 flex: 1 是 flex-grow、flex-shrink 和 flex-basis 的简写。使用简写前要先明确:元素是否应该增长、是否允许收缩、基础尺寸来自哪里。不要只因为“它能铺满”就到处添加。
Grid:协调行与列
Grid 更适合二维布局:你希望明确控制列,也可能同时关心行。
Tracebase 工作区由左侧文档区和右侧问答区组成:
.workspace {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
gap: 24px;
}
3fr 2fr 把可分配空间划成五份,左侧约占三份,右侧约占两份。这里写成 minmax(0, 3fr) 而不是单纯 3fr,是为了明确允许轨道缩小到内容默认最小宽度以下。
文档列表内部也是 Grid,但解决的是另一组关系:
.document-list li {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(150px, auto) auto;
gap: 16px;
}
- 文件名使用剩余空间,并允许收缩;
- 状态至少需要 150px,也可以根据内容增长;
- 操作按钮按自身内容决定宽度。
这比 190px 210px 180px 三个固定列更能适应真实文件名和状态消息。
Flexbox 与 Grid 怎么选
可以先问自己两个问题:
- 我主要是在一条轴上排列,还是需要协调多列关系?
- 尺寸应该由内容决定,还是由父布局统一分配?
导航、按钮组、水平工具栏通常适合 Flexbox;仪表盘主区域、表格化列表和明确的多列关系通常适合 Grid。两者可以嵌套使用,不存在“学会 Grid 后就不再用 Flexbox”。
状态、伪类与可访问性
页面不只有静态截图。用户会悬停、点击、使用键盘、等待加载,也会遇到禁用和失败状态。
交互伪类
button:hover {
background: var(--color-accent-dark);
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
:hover 表示指针悬停,:disabled 匹配真正带有 disabled 属性的表单控件。CSS 仍然只显示状态,不负责决定何时禁用按钮。
键盘焦点
a:focus-visible,
button:focus-visible,
input:focus-visible {
outline: 3px solid color-mix(
in srgb,
var(--color-accent) 35%,
transparent
);
outline-offset: 3px;
}
:focus-visible 在浏览器判断用户需要明确焦点提示时显示焦点环,尤其适合键盘导航。不要用 outline: none 删除焦点而不提供替代方案,否则键盘用户会失去当前位置。
语义状态与视觉状态
<a href="/documents" aria-current="page">知识文档</a>
.product-header a[aria-current="page"] {
font-weight: 700;
text-decoration: underline;
}
aria-current="page" 告诉辅助技术这是当前页面,CSS 同时把它画出来。语义与视觉共享同一个状态源,比额外维护一个只为变色存在的 class 更不容易失步。
颜色也不应该成为唯一提示。失败状态同时保留“处理失败”文字,当前导航同时使用字重和下划线。
响应式设计:让内容决定断点
响应式不是把桌面页面整体缩小,而是在可用空间不足时重新组织关系。
媒体查询可以在条件成立时启用一组规则:
@media (max-width: 960px) {
.workspace {
grid-template-columns: 1fr;
}
}
当视口不超过 960px,工作区从 3:2 双栏切成单栏,文档和问答面板上下排列。
断点不应该只是照抄“手机 480、平板 768、桌面 1024”这类设备表。设备会变化,内容需求更稳定。正确问题是:这个布局从什么宽度开始无法保持可读和可操作?
Tracebase 最终使用两级变化:
- 不超过 960px:主工作区改成单栏,页头上下排列;
- 不超过 640px:文档行和问答表单也改成单列。
因此 768px 下两个大面板已经上下排列,但每个面板内部仍能利用足够宽度;375px 下内部控件也会继续堆叠,按钮和输入框不再争抢一行。
响应式不仅是 Grid 列数
还要同时检查:
- 容器是否使用流式宽度并保留合理边距;
- 图片、输入框和长文本能否收缩;
- 导航是否允许换行;
- 触控目标是否足够大;
- 标题和按钮文字是否被截断;
- DOM 顺序在单栏时是否仍然符合阅读顺序。
meta viewport 也是移动端正确计算布局宽度的前提:
<meta name="viewport" content="width=device-width, initial-scale=1">
三个真实故障:从现象追到根因
下面三个问题都来自 Tracebase 的实际调试。每个案例都按照“现象、根因、证据、最小修复、防回归验证”处理。
案例一:768px 下双栏仍然生效
现象: 宽桌面正常,但 768px 截图中仍是左右双栏。文件名被压成接近逐字换行,问题输入框和回答区域非常狭窄,用户看不到完整内容。
根因: 最初的媒体查询是:
@media (max-width: 760px) {
.workspace {
grid-template-columns: 1fr;
}
}
768 大于 760,条件没有成立。CSS 语法正确,浏览器也完全按规则执行,只是规则边界不符合内容需求。
证据: 计算工作区空间:
视口宽度 768px
减去工作区左右留白 48px
减去 Grid 间隔 24px
两列可分配空间 696px
按 3:2 分配后,问答列约 278px;再减去面板 padding,内容区已经难以容纳表单与回答。DevTools 的媒体查询状态也显示 760px 条件没有命中。
最小修复: 根据组件真实宽度把断点改为 960px,不改动桌面 Grid 的比例:
@media (max-width: 960px) {
.workspace {
grid-template-columns: 1fr;
}
}
防回归验证: 重新检查宽桌面、768px 和 375px。桌面保持双栏,768px 主区域上下排列,375px 内部表单与文档行也保持完整。
这说明断点应该由内容开始失败的位置决定,而不是由某个常见设备名称决定。
案例二:导航颜色被意外改成红色
现象: 全局链接规则定义为蓝色,导航却突然变红。
a {
color: var(--color-accent);
}
body .product-header nav a {
color: #b42318;
}
根因: 第二条规则的优先级更高。它包含一个 class 选择器,而第一条只有一个元素选择器。
证据: DevTools 的 Styles 面板中,第一条 color 被划掉;Computed 面板把最终颜色来源指向第二条规则。优先级对比是:
a 0-0-0-1
body .product-header nav a 0-0-1-3
最小修复: 删除错误的故障规则,或者把真正需要的样式限定到正确组件。不要再添加一条 !important 与它对抗。
防回归验证: 刷新后确认普通链接使用品牌色,当前导航仍由 aria-current 规则获得字重与下划线;再查看 Computed 面板确认最终来源符合预期。
案例三:长文件名撑开 Grid
现象: 把文件名换成没有空格的超长字符串并设置 white-space: nowrap 后,文字先覆盖旁边状态;逐层取消保护后,整个文档面板变宽并挤开 AI 面板。
根因: Grid 项目默认有基于内容的自动最小尺寸。一个不可断行的长字符串会把自己的最小宽度向父级轨道传播,内层网格和外层网格都可能被撑开。
证据: DevTools 逐项禁用下面几层规则,可以看到故障逐步扩大:
外层轨道:minmax(0, 3fr)
外层 Grid 子项:min-width: 0
文档行第一列:minmax(0, 1fr)
文本内容:overflow-wrap: anywhere
这些规则不在同一层,因此不是重复代码。
最小修复:
.workspace {
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
}
.documents-panel,
.assistant-panel {
min-width: 0;
}
.document-list li {
grid-template-columns: minmax(0, 1fr) minmax(150px, auto) auto;
}
.document-list strong,
.document-status {
overflow-wrap: anywhere;
}
minmax(0, ...)允许 Grid 轨道继续缩小;min-width: 0允许 Grid 子项小于内容的自动最小宽度;overflow-wrap: anywhere允许文本在元素内部断行。
防回归验证: 同时测试普通文件名、长中文状态、无空格字符串和窄视口。只验证一个正常样本不足以证明溢出问题已解决。
自动测试、截图和 DevTools 各自证明什么
我们为 Tracebase 建立了静态契约测试,它可以快速确认:
- HTML 是否连接外部
styles.css; - 关键 class 和状态属性是否存在;
- 响应式断点是否写入预期规则;
- 页面是否重新出现内联样式。
但静态测试不能证明 Grid 在真实浏览器中没有溢出,也不能证明颜色对比度、键盘焦点和视觉层级合理。
不同证据解决不同问题:
| 验证方式 | 擅长证明 | 不能单独证明 |
|---|---|---|
| 静态测试 | 结构、配置、稳定契约 | 真实排版与交互体验 |
| 浏览器截图 | 特定视口的视觉结果 | 键盘流程、所有动态状态 |
| DevTools | 最终样式来源、盒模型、轨道尺寸 | 整体产品是否好用 |
| 键盘操作 | 焦点顺序和可见性 | 其他视口是否正常 |
高质量 CSS 验证应该组合使用这些证据,而不是看到测试变绿或一张桌面截图正常就结束。
Vibe coding 中如何审查 AI 生成的 CSS
AI 很适合快速生成变量、基础组件样式、Flexbox/Grid 骨架和测试初稿。真正需要程序员负责的是约束与验证。
可以按下面顺序审查:
1. 样式表是否正确连接
2. 选择器是否匹配正确 DOM,作用域是否过宽
3. 层叠、继承和状态来源是否清楚
4. 盒模型与宽高计算是否符合预期
5. Flexbox/Grid 是否允许内容收缩和换行
6. 长文本、空状态、失败状态是否完整
7. 断点是否由内容证据决定
8. hover、disabled、focus-visible 是否齐全
9. 自动测试、截图、DevTools 和键盘验证是否相互补充
AI 生成候选代码并不等于交付完成。程序员需要能解释关键规则,能指出错误状态和溢出风险,能根据证据做局部修改,也能拒绝为了压过某条规则而滥用 !important。
这正是 AI-native 开发的能力标准:不要求脱离 AI 低效率地背写全部样板代码,但必须对需求、边界、失败路径和最终体验负责。
完整示例:可直接运行的 Tracebase 页面
在同一个目录创建 index.html 和 styles.css,然后直接用浏览器打开 index.html。调整 DevTools 的视口宽度,就能观察桌面、768px 和 375px 三种布局。
index.html
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tracebase 企业知识库</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="product-header">
<a class="brand" href="/">Tracebase</a>
<nav aria-label="主导航">
<ul>
<li>
<a href="/documents" aria-current="page">知识文档</a>
</li>
<li><a href="/conversations">问答记录</a></li>
</ul>
</nav>
</header>
<main class="workspace">
<section
class="documents-panel"
aria-labelledby="documents-title"
>
<h1 id="documents-title">知识文档</h1>
<p class="section-intro">管理用于企业问答的内部资料。</p>
<button type="button">上传文档</button>
<ul class="document-list">
<li>
<strong>产品手册.pdf</strong>
<span class="document-status" data-state="ready">
已就绪
</span>
</li>
<li>
<strong>值班流程.md</strong>
<span class="document-status" data-state="processing">
处理中
</span>
</li>
<li>
<strong>过期接口.txt</strong>
<span class="document-status" data-state="failed">
处理失败:文件编码无法识别
</span>
<button type="button">重新处理</button>
</li>
</ul>
</section>
<section
class="assistant-panel"
aria-labelledby="assistant-title"
>
<h2 id="assistant-title">AI 问答</h2>
<p class="section-intro">
回答只依据已经处理完成的知识文档。
</p>
<form class="question-form">
<label for="question">你的问题</label>
<input
id="question"
name="question"
type="text"
placeholder="例如:值班故障如何升级?"
>
<button type="submit">发送问题</button>
</form>
<article class="answer" aria-labelledby="answer-title">
<h2 id="answer-title">回答</h2>
<p>
严重故障需要先通知当班负责人,再按照升级矩阵联系服务负责人。
</p>
<h3>引用来源</h3>
<ol>
<li>
<a href="/documents/on-call#section-4">
值班流程,第 4 节
</a>
</li>
</ol>
</article>
</section>
</main>
</body>
</html>
styles.css
:root {
--color-canvas: #f4f6f8;
--color-surface: #ffffff;
--color-ink: #18212b;
--color-muted: #5f6b78;
--color-border: #d8dee6;
--color-accent: #1d5fd1;
--color-accent-dark: #174ba5;
--color-success: #18794e;
--color-warning: #8a6100;
--color-danger: #b42318;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--radius: 6px;
--content-max: 1180px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--color-canvas);
color: var(--color-ink);
font-family:
Inter, ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
}
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.product-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-5);
width: 100%;
padding-block: var(--space-4);
padding-inline: max(
var(--space-5),
calc((100% - var(--content-max)) / 2)
);
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
}
.brand {
color: var(--color-ink);
font-size: 22px;
font-weight: 700;
}
.product-header ul {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
margin: 0;
padding: 0;
list-style: none;
}
.product-header a[aria-current="page"] {
color: var(--color-accent-dark);
font-weight: 700;
text-decoration: underline;
text-underline-offset: 6px;
}
.workspace {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
gap: var(--space-5);
width: min(
calc(100% - 2 * var(--space-5)),
var(--content-max)
);
margin: var(--space-6) auto;
}
.documents-panel,
.assistant-panel {
min-width: 0;
padding: var(--space-5);
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
h1,
h2,
h3,
p {
margin-top: 0;
}
.section-intro {
color: var(--color-muted);
}
button,
input {
border: 1px solid var(--color-border);
border-radius: var(--radius);
font: inherit;
}
button {
padding: 10px 16px;
background: var(--color-accent);
color: var(--color-surface);
cursor: pointer;
}
button:hover {
background: var(--color-accent-dark);
}
button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
a:focus-visible,
button:focus-visible,
input:focus-visible {
outline: 3px solid color-mix(
in srgb,
var(--color-accent) 35%,
transparent
);
outline-offset: 3px;
}
.document-list {
margin: var(--space-5) 0 0;
padding: 0;
list-style: none;
}
.document-list li {
display: grid;
grid-template-columns:
minmax(0, 1fr) minmax(150px, auto) auto;
align-items: center;
gap: var(--space-4);
padding: var(--space-4) 0;
border-bottom: 1px solid var(--color-border);
}
.document-list strong,
.document-status {
overflow-wrap: anywhere;
}
.document-status[data-state="ready"] {
color: var(--color-success);
}
.document-status[data-state="processing"] {
color: var(--color-warning);
}
.document-status[data-state="failed"] {
color: var(--color-danger);
}
.question-form {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--space-3);
}
.question-form label {
grid-column: 1 / -1;
font-weight: 600;
}
.question-form input {
min-width: 0;
width: 100%;
padding: 10px 12px;
}
.answer {
margin-top: 28px;
padding-top: var(--space-5);
border-top: 1px solid var(--color-border);
}
@media (max-width: 960px) {
.product-header {
align-items: flex-start;
flex-direction: column;
padding-inline: var(--space-4);
}
.workspace {
grid-template-columns: 1fr;
width: min(
calc(100% - 2 * var(--space-4)),
var(--content-max)
);
margin-block: var(--space-4);
}
.documents-panel,
.assistant-panel {
padding: var(--space-4);
}
}
@media (max-width: 640px) {
.document-list li,
.question-form {
grid-template-columns: 1fr;
}
.document-list li {
gap: var(--space-2);
}
.document-list button {
justify-self: start;
}
.question-form label {
grid-column: auto;
}
}
初学者 CSS 检查清单
写完或接收 AI 生成的样式后,可以逐项确认:
- HTML 是否通过正确路径连接了外部样式表?
- 你能指出每条关键规则的选择器、属性和值吗?
- class 表达组件,状态属性表达状态,两者是否混在一起?
- DevTools 中被划掉的规则为什么输了?
- 最终值来自直接匹配、继承,还是浏览器默认样式?
- 全局是否使用了可预测的
border-box? - 固定宽度是否可能超过父容器或窄屏?
- 当前场景真的需要 Flexbox 或 Grid,还是普通文档流已经足够?
- Grid 轨道和子项是否允许收缩?长文本能否断行?
- 断点来自内容失败证据,还是随手抄来的设备宽度?
- hover、disabled、focus-visible、空状态和失败状态是否齐全?
- 是否检查了桌面、768px、375px、长内容和键盘操作?
- 自动测试、浏览器和 DevTools 的证据是否相互印证?
总结
CSS 的核心不是记住几百个属性,而是理解一条连续链路:
DOM 提供结构和状态
-> 选择器找到元素
-> 层叠与继承决定最终值
-> 盒模型决定尺寸
-> 普通流、Flexbox、Grid 决定排列
-> 媒体查询处理空间变化
-> DevTools 与真实操作验证结果
Tracebase 的三个故障也说明,页面“看起来有样式”只是起点。固定宽度可能暗中增长,语法正确的断点可能不符合真实内容,更高优先级的选择器可能覆盖预期规则,长字符串也可能跨越多层 Grid 传播尺寸压力。
在 vibe coding 工作流中,AI 可以快速写出候选 CSS,而程序员的价值体现在能否读懂这条链路、发现错误边界、要求正确状态,并用可重复的证据完成修复。掌握这些机制以后,忘记某个具体属性并不可怕,因为你知道应该去哪里查、如何验证,以及怎样判断答案是否可靠。

没有回应