CSS 用来描述文档应该怎样呈现。HTML 负责标题、段落、链接和表单等结构与含义;CSS 决定这些内容在屏幕、打印页面或其他媒介中的颜色、尺寸、间距和布局。先分清两者的职责,后面组织和维护样式会轻松很多。
我们从一条最小的 CSS 规则开始,看浏览器怎样解析样式、怎样为元素生成盒,以及 display 到底改变了什么。随后再把几种样式接入方式放在一起比较。这样学完,你不只知道代码怎么写,也能判断浏览器为什么会得到当前结果。
早期网页主要用来表达结构,页面外观很大程度上取决于浏览器和用户偏好。CSS 提供了一套声明式语法,让网站开发者能够定义页面外观,同时没有排除浏览器默认规则和用户样式。来自不同位置的声明会经过层叠机制比较,最后才得出实际使用的值。
CSS 后来不再以一个大版本包揽所有功能,而是拆成相对独立的模块。选择器、颜色、字体和布局等模块可以按各自的节奏更新。所以今天讨论一个功能时,更准确的方式是说明它属于哪个模块、哪个级别,并核对这个功能的浏览器支持情况,而不是笼统地称为“CSS3”。
CSS 不只用于普通网页。只要界面按类似文档的结构呈现,就可能用 CSS 控制外观。学习时可以把重点放在语法、格式化模型和层叠机制上,不必背诵某个版本包含了哪些功能。
样式表由一条条规则组成。下面这条规则会选中所有一级标题,并设置文字颜色和背景颜色:
h1 {
color: maroon;
background-color: #fff4b8;
}花括号前的 h1 是选择器,它回答“这条样式要用在哪些元素上”。花括号里的内容是声明块。声明块中的每条声明都由属性和值组成:冒号隔开属性和值,分号隔开相邻的声明。

选择器可以只是一个元素名,也可以组合类名、属性、状态和结构关系。写法可能越来越复杂,但它始终只负责匹配元素。声明块负责说明匹配之后怎样呈现。由于匹配对象和呈现方式彼此分开,同一组声明可以复用在许多元素上。
一个声明块里可以只有一条声明,也可以有很多条。最后一条声明末尾的分号通常可以省略,不过建议保留。以后追加属性时不必回头补分号,也少了一个容易漏写的地方。
试着调整颜色、字号和圆角。两个标题会一起变化,因为它们都被同一个选择器匹配到了。
阅读旧代码时,你可能会遇到以 -webkit-、-moz-、-ms- 或 -o- 开头的属性和值。这些前缀过去常用来标记实验功能或特定浏览器的实现,例如:
.card {
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}前缀通常写成“连字符 + 浏览器标识 + 连字符”。过去,一些网站直接依赖尚未稳定的实验实现,后来便留下了不少兼容问题。现在写项目时,不要凭记忆列出一排前缀。先查清目标浏览器是否需要,再让构建工具按兼容范围生成。标准写法仍要保留,通常放在带前缀的写法之后。
CSS 基本不在意规则之间有多少空白。规则内部的大多数连续空格、制表符和换行,也只会被当作解析所需的分隔。因此,下面两段代码通常等价:
h1{color:navy;background:white}h1 {
color: navy;
background: white;
}正因为如此,生产环境可以压缩 CSS,删掉不影响解析的字符来减小文件体积。但多数空白不影响解析,不代表所有空白都能删除。某些空白负责隔开两个标记,删掉后它们可能被解析成另一个整体;字符串里的空白本身也是值的一部分。源码先保持清楚易读,压缩交给工具处理就好。

CSS 只有一种标准注释:/* ... */。它可以跨越多行:
/* 卡片的基础外观:供列表与详情页复用 */
.card {
padding: 1rem;
border: 1px solid #dbe3ef;
}CSS 注释不能嵌套。如果用一对注释符包住一大段已经含有注释的代码,解析器遇到第一个 */ 时就会结束注释,后面的内容可能重新参与解析。普通 CSS 不支持 // 行注释,# 也不是注释起始符。
浏览器解析样式时会移除注释,它甚至不一定能替代两个标记之间的空白。写注释时,最好说明某段代码为什么存在,或者记录它的兼容背景。代码本身已经说清楚的内容,不必再逐字翻译一遍。
独立的 .css 文件只能放 CSS 规则、注释和 @ 规则,不能写 <p>、<link> 等 HTML 标记。浏览器会按照 CSS 语法解析外部样式表,混入 HTML 后,相关内容可能直接失效。
一些很早期的页面会在 <style> 中用 HTML 注释包住 CSS,以避开不认识样式元素的浏览器。现代页面已经不需要这样做。记住边界即可:HTML 文件组织文档,CSS 文件保存样式。
HTML 元素组成文档结构。浏览器会为参与视觉格式化的元素生成一个或多个盒,CSS 控制的尺寸、位置和装饰,实际都落在这些盒上。元素在文档树中的语义身份与它在布局中的盒角色有关联,但两者不是一回事。
替换元素显示的内容来自元素外部,浏览器会用外部对象或平台控件替代元素自身的内容。比如,img 的图像数据来自 src;许多 input 的外观则由控件类型和浏览器实现共同决定。替换元素通常有内在宽高,所以它们的尺寸计算与普通文本容器有所不同。
非替换元素的内容由文档自身提供,例如段落里的文字、span 中的短语。浏览器根据这些内容和样式生成相应的盒。这里的分类只看“内容从哪里来”,并不能直接判断元素是块级还是行内级。

块级盒通常沿块方向依次排列,常见表现是从新的一行开始。行内级盒会参与行内格式化,和文字一起排在行盒中。p 默认通常生成块级盒,span 默认通常生成行内级盒,不过 CSS 可以通过 display 改变生成盒的角色。
这里很容易产生两个误解:块级不等于宽度永远是 100%,行内也不等于不能设置样式。它们说的是盒怎样参与格式化上下文。display 的变化不会改写 HTML 语义,把 span 设为 block 后,它依然是 span。
display 的两个层次理解现代 display 时,可以分别看它的外部角色和内部布局。外部角色说明这个盒怎样参与周围的布局,内部布局说明它的子元素怎样排列。例如,inline-block 对外表现得像行内级盒,对内则是一个可以容纳普通盒布局的容器;flex 和 grid 会明确选择相应的内部布局模型。
常见值有 block、inline、inline-block、flex、grid、list-item、table 和 none。使用 display: none 后,元素不会生成布局盒,通常也不会进入可访问性树。它的效果不只是让元素变得透明。
CSS 必须和文档建立关联,样式才会出现在页面上。我们常用的方式有外部样式表、文档内的 <style>、@import,以及元素上的 style 属性。服务器也能通过 HTTP Link 响应头关联样式表,不过这种方式不够直观,通常只会出现在特殊环境里。

<link>外部样式表把规则保存在纯文本 .css 文件中,再通过 HTML <head> 中的 <link> 建立关联:
<link rel="stylesheet" href="/styles/site.css">rel="stylesheet" 说明当前链接指向样式表,这是浏览器正确识别它的关键。href 提供样式表 URL,既可以写相对地址,也可以写绝对地址。HTML 默认把 CSS 样式表视为 text/css,旧式的 type="text/css" 通常可以省略。
你还可以用 media 限制样式表适用的媒介或条件:
<link rel="stylesheet" href="screen.css" media="screen">
<link rel="stylesheet" href="print.css" media="print">省略 media 时,样式表默认适用于 all。一个文档可以链接多个样式表,浏览器会加载符合条件的文件,再把其中的规则一起纳入层叠。外部样式表便于集中维护,也能缓存并跨页面复用,所以大多数项目会优先使用它。
不带 title 的普通样式表可以看作持久样式,会一直参与页面呈现。带有 title 且 rel="stylesheet" 的样式表可以组成首选样式集;如果使用 rel="alternate stylesheet" 并配合相同的 title,则可以描述候选样式集,比如一个“大字号”主题。
浏览器很少提供好用的候选样式表切换界面,因此产品里的主题切换通常由脚本、类名或自定义属性实现。不过,知道 title 会改变样式表的分组语义,仍然有助于读懂旧页面。多个原本需要同时生效的基础文件,不要随意添加不同的 title。
<style><style> 会把样式表直接写进当前 HTML,一般放在 <head> 中:
<style media="screen">
.notice { color: #9a3412; }
</style>这种方式适合独立示例、关键的首屏样式,或者确实只属于某个页面的少量规则。这里的 media 和 <link> 中的用途相近,type 在 HTML 中同样可以省略。如果规则越来越多,或者其他页面也需要使用,就该把它们拆到外部文件中。
@import 的位置、媒体与层@import 用于在一个样式表中加载另一个样式表:
@import url("tokens.css");
@import url("print.css") print;
@import url("base.css") layer(base);
body { color: #1f2937; }@import 必须写在普通样式规则之前。它前面只能出现 @charset、用于创建层顺序的 @layer 等特定规则。位置写错后,符合规范的浏览器会忽略这条导入。URL 可以是相对地址或绝对地址,也可以附加媒体条件,还能把导入的内容放进命名层或匿名层。
@import 和 <link> 不能简单互换。前者属于 CSS 语法,可以在外部 CSS 中继续组织依赖并指定层;后者属于 HTML,直接描述文档与样式表的关系。如果项目对加载性能敏感,通常会让打包工具合并依赖,以免产生不必要的串行请求。
Link 响应头服务器也可以在 HTTP 响应中发送 Link 头,把样式表与文档关联起来。它的语义类似 HTML 中的 <link>。例如,服务器配置可能会产生下面的响应头:
Link: </ui/testing.css>; rel=stylesheet; type=text/css这种关联方式不容易从页面标记中发现,浏览器支持和调试体验也不如 HTML <link> 稳定。受控的开发环境或特定基础设施实验可能会用到它;公开站点更适合直接输出 <link> 元素。即使样式由服务端动态决定,也可以生成对应的 HTML 标记。
style 属性行内样式直接把声明写在单个元素上,不需要选择器和花括号:
<p style="color: maroon; background: #fff4b8;">一段提示文字</p>这些声明只影响当前元素。属性值里只能放原本写在规则花括号中的内容,不能写完整规则、选择器或 @import。大量使用行内样式会让呈现信息散落在标记中,不利于集中维护,也难以缓存和复用,所以它不适合成为常规架构。组件根据运行时数据计算少量值时可以谨慎使用;更常见的状态切换,优先通过类名和样式表完成。
把前面的规则放进实际项目时,可以按下面的顺序处理:
排查时常见的错误有:漏写 rel="stylesheet",把 HTML 标记放进 .css,把 @import 写在普通规则之后,误以为替换元素一定是块级,把 display: none 当成单纯透明,或者认为所有空白都可以删除。
样式“完全没生效”时,先确认资源有没有加载,再检查语法边界和选择器匹配,最后看层叠结果。先别急着提高优先级或添加 !important,这样做很可能把真正的问题藏起来。
回头看这一篇,最需要分清的是几组边界。选择器负责匹配对象,声明块负责描述呈现;浏览器按照明确的空白、注释和语法边界解析样式;元素的内容来源、显示角色和 HTML 语义则属于不同的分类维度。至于样式接入,<link>、<style>、@import、HTTP 头和 style 属性各有位置,大多数项目仍会把可复用的规则放进外部样式表。
把这些边界弄清楚后,就可以继续学习复杂选择器、层叠优先级、继承和盒模型。遇到样式结果不符合预期时,也可以顺着浏览器的处理过程排查,而不是反复修改到它碰巧生效。