
CSS 声明看起来就是“属性名加一个值”,但浏览器要处理的远不止这几个字符。一个值属于什么类型、以什么为参照、在哪个阶段计算,以及能不能用于当前属性,都会影响最后的页面。比如 24px、1.5rem、50% 都能描述尺寸,参照基准却可能完全不同。none、"none" 与 --none 拼写相近,在语法中承担的角色也不一样。颜色同样不只是一串十六进制字符,它还可以表达透明度、感知亮度和宽色域。
这篇文章会把常见的值类型放到同一张地图里。我们不只看单位怎么写,也会顺着浏览器的计算过程,弄清一个值为什么会被接受、拒绝或重新计算。
判断一个 CSS 值时,可以先问三个问题:它是什么类型,相对谁计算,能不能用于当前属性。只看写出来的字符,很容易漏掉浏览器实际采用的解析规则。
关键字是 CSS 预先定义的词,书写时不用加引号。它到底表示什么,要看所在的属性。font-style: normal 里的 normal 表示常规字形;到了 letter-spacing: normal,它表示由浏览器决定正常字距。同一个词放进不同属性,不一定有相同含义。none 也不能简单理解成数字 0:text-decoration: none 是关闭装饰线,换成 text-decoration: 0 就不成立。
所有属性都接受下面五个全局关键字:
inherit:强制使用父元素该属性的计算值,即使此属性通常不继承。initial:回到规范为该属性定义的初始值,而不是回到作者记忆中的“浏览器默认样式”。unset:若属性天然继承则等同 inherit,否则等同 initial。revert:撤销当前样式来源的影响,保留更低来源(如用户样式、浏览器样式)的结果。revert-layer:只撤销当前级联层的贡献,让其他层继续参与级联。.toolbar {
color: white;
background: #2155a3;
}
.toolbar a {
color: inherit; /* 链接跟随工具栏前景色 */
}
.demo {
margin: unset; /* margin 不继承,因此回到 initial */
font-family: unset; /* font-family 会继承,因此取父元素值 */
}revert 和 initial 很容易混淆,我们用按钮来区分。假设浏览器默认样式给按钮加了边框,all: initial 会让相关属性回到规范定义的初始值;all: revert 更接近“撤掉作者写的样式”,所以浏览器提供的按钮外观仍可能保留下来。
all:批量重置并不等于清空一切all 很特殊,它只接受全局关键字,几乎可以一次影响所有普通属性。不过,direction、unicode-bidi 和自定义属性不在它的重置范围内。我们常用 all: unset 隔离第三方组件,或者一次撤掉大量作者样式。代价是 display、box-sizing、字体和边框等也会一起被处理,所以写下它以后,还要把组件需要的基础样式补回来。
.isolated-button {
all: unset;
box-sizing: border-box;
display: inline-flex;
align-items: center;
cursor: pointer;
}all: inherit 也不是“只继承父元素明确写过的几项”。它会强制几乎所有属性都从父元素取值,连原本不继承的背景、边框和尺寸也会跟着继承,影响范围往往比看上去大。
交互实验:比较五种重置行为
CSS 字符串要用成对的直引号包起来,单引号和双引号都可以,但两端必须一致。如果字符串内部还要写同类引号,就用反斜杠转义。弯引号可以出现在字符串内容里,却不能拿来界定字符串。
.quote::before { content: "他说:\"继续学习。\""; }
.line::after { content: "第一行\A第二行"; white-space: pre; }反斜杠后面紧跟源码换行时,这个换行会从字符串中被移除。如果你想插入一个真正的换行字符,应写 \A,并配合允许换行显示的 white-space。
标识符是不加引号的名称,动画名、网格线名和计数器名都属于这一类。自定义标识符区分大小写,不能以数字开头,也要避开 none、initial、inherit 等关键字。自定义属性名属于虚线标识符,必须以 -- 开头,同样区分大小写。
@keyframes cardEnter { from { opacity: 0; } }
.card { animation-name: cardEnter; }
/* cardEnter 与 CardEnter 是两个不同标识符 */资源地址写在 url() 中。参数可以使用单引号或双引号,满足语法限制时也能省略引号,但函数名和左括号之间不能有空格。这里最容易踩坑的是相对路径:外部样式表中的相对 URL,以样式表文件所在位置为基准,不是以引用这份样式表的 HTML 为基准。
/* /styles/main.css 中 */
@import url("components/card.css");
.hero { background-image: url("../images/hero.webp"); }<image> 也不只指图片文件 URL。渐变属于图像值,image-set() 可以按分辨率等条件提供候选资源,cross-fade() 用来表达图像混合,paint() 可以连接 Paint Worklet。部分较新的图像函数仍有兼容性差异,用在生产环境前,要先确认目标浏览器的支持情况,并准备可靠的回退。
.logo {
background-image: image-set(
url("logo.png") 1x,
url("logo@2x.png") 2x
);
}fr<integer> 表示整数,可以带正负号,例如 12、-3。<number> 的范围更宽,还允许小数和科学计数法,例如 2.75、1.2e3。具体能写到什么范围,以及越界后怎样处理,都由属性决定。opacity: 1.7 会被钳制到有效边界;有些属性遇到范围外的值,则会让整条声明失效。z-index 只接受整数,flex-grow 接受一般数字。
百分比一定要依赖某个参照值,可参照物不是固定的,而是由属性定义。width: 50% 通常参考包含块宽度,font-size: 120% 参考父元素字号,位移中的百分比又可能参考元素自身。因此,“百分比都相对父元素”这句口诀并不可靠。
fr 表示网格容器中剩余空间的一份,它不是长度。grid-template-columns: 1fr 2fr 会先扣除固定轨道和间隙,再把剩余空间按 1:2 分配。正因为 fr 不属于 <length>,它不能和 px 当作同类量放进 calc() 相加。
CSS 的绝对长度包括 in、cm、mm、Q、pt、pc 和 px。它们之间有固定换算关系:1in = 2.54cm = 25.4mm = 96px = 72pt = 6pc,1cm = 40Q。打印样式通常更适合用厘米、毫米或点来贴近物理排版,屏幕界面则更常使用 px。
一个 CSS 像素并不等于一个硬件发光点。浏览器会用“参考像素”维持大致一致的视觉角度,再根据设备密度、缩放和输出介质,把它映射到一个或多个设备像素。所以到了高密度屏幕,16px 文字不会自动缩成肉眼难辨的小点阵。长度为零时可以省略单位,例如 margin: 0;非零长度必须带单位。
@media print {
article { width: 160mm; font-size: 11pt; }
}
dpi、dpcm、dppx分辨率描述的是单位长度或单位 CSS 像素对应多少显示点。dpi 表示每英寸点数,dpcm 表示每厘米点数,dppx 表示每 CSS 像素的点数。CSS 规定 1dppx = 96dpi。这些单位常出现在媒体查询里,用来选择更合适的图像或细节策略。
@media (min-resolution: 2dppx) {
.icon { background-image: url("icon@2x.png"); }
}分辨率查询反映的是输出密度,并不会让 16px 字体在 2dppx 屏幕上变成一半大小。CSS 像素的视觉尺度仍由浏览器的映射机制维护。
字体相对单位直接把排版本身当作标尺:
em:在一般属性中相对当前元素字号;但在 font-size 自身上,相对父元素字号。ex:当前字体小写字母 x 的高度。ch:数字 0 字形的推进尺寸;比例字体里并不代表所有字符都恰好等宽。ic:汉字“水”的推进尺寸,适合表意文字排版;无法计算时通常按 1em 处理。cap:大写字母高度;lh:当前元素计算后的行高。根相对单位把参照点固定在文档根元素。rem 对应根字号,rex、rch、ric、rcap、rlh 分别对应根元素的 x 高度、0 推进尺寸、“水”推进尺寸、大写高度和行高。这样一来,深层嵌套时就不会出现 em 层层相乘的问题。
html { font-size: 100%; }
.card { padding: 1.25rem; }
.card__title { font-size: 1.25em; margin-block-end: 0.75em; }
.article { max-width: 68ch; }假设写了 html { font-size: 75%; },用户默认字号是 16px,此时 1rem 等于 12px。如果用户把默认字号改成 20px,1rem 就会变成 15px。用百分比设置根字号,仍能让用户的字号偏好参与计算。
交互实验:亲手改变参照物
vw 和 vh 分别是默认视口宽度、高度的 1%。vi、vb 按书写模式的行内轴和块轴计算;vmin 取宽高中较小一边的 1%,vmax 取较大一边的 1%。这些单位不受属性轴向限制,因此 width: 25vh 完全合法,它表示宽度等于视口高度的四分之一。
移动浏览器的地址栏会展开和收起,视口高度也随之变化。为了解决这个问题,视口单位又分出三套变体:
sv*:小视口,按浏览器界面完全展开时的可用区域计算,适合保证内容不被遮挡。lv*:大视口,按界面最大收起时计算,适合需要铺满最大区域的视觉层。dv*:动态视口,跟随当前可见区域实时变化,适合沉浸式面板,但变化可能触发布局抖动。.screen {
min-height: 100svh;
}
.live-panel {
height: 100dvh;
}calc() 的类型系统CSS 函数会出现在许多场景中。url() 处理资源,rgb()、hsl()、color() 产生颜色,linear-gradient() 产生图像,translate() 和 rotate() 描述变换,blur() 和 drop-shadow() 用于滤镜,counter() 读取计数器,env() 读取环境变量,var() 读取自定义属性。函数名只是入口,参数能写什么,仍要服从各自的语法定义。
calc() 可以对长度、百分比、角度、时间、频率和数字等做运算,同时会检查类型:
5em + 20px 都是长度,可以计算,5em + 2 不可。2rem * 3 有效,2rem * 3rem 会产生不存在的面积单位,因此无效。+、- 两边必须留空格;calc(100%-2rem) 会解析失败。calc(),但通常可扁平化以提升可读性。.layout {
width: calc(90% - 2rem);
padding-inline: calc(var(--space) * 2);
}min()、max()、clamp() 与 attr()min() 会选择计算结果最小的候选值,所以它常用来表达“最大不超过”。max() 选择最大的候选值,适合表达“至少为”。如果上下边界都要控制,可以用 clamp(下限, 理想值, 上限) 同时写出边界和弹性目标。
.figure { width: min(25vw, 200px); }
.banner { min-height: max(100px, 15vh, 5rem); }
.title { font-size: clamp(1.25rem, 1rem + 2vw, 3rem); }这里说的“最小”,指的是数轴上更接近负无穷的一项,不是绝对值更接近零。如果 clamp() 计算出的上限小于下限,以下限为准。给字体设置固定的像素上限,可能妨碍用户放大文字。设计流体字号时,最好保留 rem 成分,并实际测试浏览器缩放。
attr() 可以读取当前元素的 HTML 属性值,目前最成熟的用途是生成内容:
a[data-label]::after {
content: "(" attr(data-label) ")";
}不要预设所有浏览器都能在任意属性中,把 attr() 的返回值解析成指定类型。使用扩展语法前,要先核对兼容性并准备回退。

命名颜色一眼就能读懂,适合原型,或者少量语义明确的颜色。transparent 表示完全透明,计算时可以理解为透明黑。currentColor 读取当前元素 color 的计算值,用它可以让边框、图标自然跟随文字颜色。
.tag {
color: rebeccapurple;
border: 1px solid currentColor;
background: transparent;
}RGB 的现代语法使用空格分隔通道,再用斜杠分隔透明度,例如 rgb(34 87 160 / 80%)。旧式逗号语法依然常见,但在同一个旧式三通道值里,百分比和数值不能混用。通道通常映射为 0–255 或 0%–100%,超出范围的值会按规则钳制。十六进制有 #RRGGBB、#RGB、#RRGGBBAA、#RGBA 四种写法。短写形式会复制每一位,例如 #6fa 会展开成 #66ffaa。
HSL 用色相角、饱和度和明度描述颜色。饱和度为 0% 时,无论色相是多少,结果都落在灰阶。明度 0% 是黑色,100% 是白色。HWB 改用色相、白度和黑度,只接受空格分隔的现代语法,也可以通过斜杠增加透明度。
.a { color: hsl(210 65% 42% / 0.8); }
.b { color: hwb(210 20% 25% / 80%); }Lab/LCH 更关注感知上的均匀性。Lab 使用亮度以及 a、b 两条颜色轴,LCH 则把后两项改写为色度和色相角。Oklab/Oklch 又改善了色相与色度的均匀性,很适合渐变和程序化调色。color() 可以明确指定色彩空间,例如 display-p3、rec2020、xyz-d50。有效颜色如果超出设备色域,会被映射到设备可显示的范围。使用宽色域颜色时,仍应提供 sRGB 回退。
.badge {
color: rgb(118 40 150);
color: oklch(55% 0.22 320);
}
.wide-gamut {
background: rgb(0 180 120);
background: color(display-p3 0 0.76 0.45);
}设置前景色时,也要一起检查背景和对比度。虽然 color 会继承,链接和表单控件仍可能受到浏览器默认样式、系统原生控件绘制的影响。因此,只给父元素设置颜色,并不能保证所有控件内部的细节都一致。
交互实验:颜色与流体尺寸
角度有四种单位,换算关系是 360deg = 400grad = 2πrad = 1turn。因此,45deg 等于 50grad、约 0.785rad 或 0.125turn。角度即使为零也应保留单位,例如 rotate(0deg)。连续旋转时,用 turn 往往更容易读懂。
时间使用 s 或 ms,例如 2.4s = 2400ms。频率使用 Hz 或 kHz,例如 128Hz = 0.128kHz。这两类值即使为零,也不能省略单位。频率在常规视觉样式中不算常见,但它仍是 CSS 定义的值类型。
比率由两个正数和一条斜杠组成,例如 aspect-ratio: 16 / 9。第一项是行内尺寸,第二项是块尺寸。这里不能改用冒号,也不该把它换算成单个小数,因为语法需要的是 <ratio>。
位置值可以混用关键字、百分比和长度。单独一个 left 等于 left center;写两个数值时,前一个表示水平位置,后一个表示垂直位置。四值语法把偏移量放在边缘关键字之后:right 10px bottom 30px 表示距右边 10px、距下边 30px。三值语法中缺少的偏移量按零处理,所以 right 20px top 等价于 right 20px top 0。
.media {
aspect-ratio: 16 / 9;
background-position: right 10px bottom 30px;
transition-duration: 240ms;
transform: rotate(0.125turn);
}
自定义属性以 -- 开头,它的值不限定为颜色或长度,保存的是一段等待替换的标记序列,使用时再通过 var() 读取。名称区分大小写,所以 --main-color 和 --Main-color 是两个不同的变量。
:root {
--brand-color: #3159a6;
--space: 0.75rem;
--level: 1;
}
aside {
--brand-color: #d65b24;
}
.card {
color: var(--brand-color);
margin-inline-start: calc(var(--space) * var
自定义属性会参与级联,并且默认继承。因此,在局部重新定义它,就能形成一个主题作用域。比如嵌套列表只需要改变 --level,同一条公式便可以算出 0.75rem、1.5rem、2.25rem 的缩进,不必逐层手算。
var(--name, fallback) 中,逗号后面的内容是一个完整的回退值。如果需要多级回退,就要嵌套 var():
.dialog {
color: var(--dialog-color, var(--brand-color, maroon));
}只有自定义属性缺失或自身无效时,回退值才会启用。如果变量存在,只是在替换到目标属性后类型不匹配,后面的回退并不会接手。比如 --indent: 5vw; color: var(--indent, gray) 中,变量确实存在,替换后却得到无效的 color: 5vw,整条声明会在计算值阶段失效。对继承属性,结果通常表现为继承;对不继承属性,则回到初始行为。
自定义属性的回退不能代替类型检查。变量已经存在,只是类型不适用于目标属性时,var() 内的回退不会生效。给设计令牌命名时,最好直接体现类型,例如 --color-brand、--space-card、--duration-fast。
回头看这一整套值系统,可以把它分成三层。文本层包括关键字、字符串、标识符、URL 和图像;数量层包括数字、百分比、长度、角度、时间、频率、比率和位置;到了计算层,函数、自定义属性、继承和级联会把前两层组合起来,得到浏览器最终使用的结果。
写 CSS 值时,不必固定偏爱 px、rem 或某一种颜色格式,关键是根据用途选择参照系。组件内部的节奏可以用 em,全局间距可以用 rem,文本行宽可以用 ch;移动端高度要在 svh 和 dvh 之间按场景取舍,流体范围可以交给 clamp(),主题和值复用则适合使用带有类型意识的自定义属性。以后遇到复杂声明,我们仍可以回到三个问题:它是什么、相对谁、何时计算。把这三件事说清楚,计算结果就更容易判断。