CSS 基础入门:从零搭建响应式 Tracebase 知识库工作台

CSS 经常被误解成“给网页换颜色的工具”,于是初学者容易陷入两个极端:要么背诵大量属性,却不知道它们为什么没有生效;要么让 AI 一次生成整份样式,只要页面看起来像成品就不再检查。这篇文章换一种方式学习 CSS。我们会从一条最小规则开始,逐步理解选择器、层叠、继承、盒模型、单位、Flexbox、Grid、响应式和可访问性,再把它们落到 Tracebase 企业知识库工作台中。最后还会复盘三个真实故障,并附上可以直接运行的完整 HTML 与 CSS。

从 CSS 基础规则到 Tracebase 响应式知识库工作台的完整实战

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,也就是层叠:浏览器必须为同一个属性选出最终获胜的声明。

可以先记住一个实用的判断顺序:

  1. 声明是否有效,媒体查询等条件是否成立;
  2. 来源和重要性是否不同,例如普通作者样式与 !important
  3. 选择器优先级谁更高;
  4. 优先级相同时,源码中后出现的规则获胜。

在日常项目中,选择器优先级可以粗略理解为下面四组:

内联样式 | 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

单位没有绝对的好坏,关键是它表达的约束是否正确。

单位或函数含义适合场景常见风险
pxCSS 像素边框、图标、稳定的小尺寸大量固定宽度难以响应
%相对包含块流式宽度高度百分比常因父级无明确高度而失效
rem相对根元素字号字号、间距需要统一根字号策略
vw/vh相对视口全屏区域、少量视口约束移动浏览器工具栏会影响高度
frGrid 剩余空间份数网格轨道比例只能用于 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: 1flex-growflex-shrinkflex-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 怎么选

可以先问自己两个问题:

  1. 我主要是在一条轴上排列,还是需要协调多列关系?
  2. 尺寸应该由内容决定,还是由父布局统一分配?

导航、按钮组、水平工具栏通常适合 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.htmlstyles.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,而程序员的价值体现在能否读懂这条链路、发现错误边界、要求正确状态,并用可重复的证据完成修复。掌握这些机制以后,忘记某个具体属性并不可怕,因为你知道应该去哪里查、如何验证,以及怎样判断答案是否可靠。

没有回应

    发表回复

    您的邮箱地址不会被公开。 必填项已用 * 标注