CSS 教程:从选择器到容器查询

一、CSS 是什么:三种引入方式与层叠起点

HTML 决定「这是什么内容」,CSS 决定「它怎么呈现」。CSS 的全部机制都建立在一个前提上:同一个元素的同一个属性可以被多条规则命中,最终只有一条赢。理解这场裁决(层叠,cascade),后面所有「我明明写了却没生效」的问题都有了解法。

外链、内联块与 @import:各自的代价

<!-- ① 外链样式表:正式项目的唯一正确做法 -->
<link rel="stylesheet" href="/css/main.css">

<!-- ② 页面内 style 块:单页 demo、首屏关键 CSS -->
<style> body { max-width: 700px; margin: 0 auto; } </style>

<!-- ③ @import:写在 CSS 文件顶部 -->
<style> @import url("/css/reset.css"); </style>

<!-- ④ 元素上的 style 属性:权重最高,只用于必须动态计算的值 -->
<div style="width: 42%">…</div>
方式优点代价
link 外链多页共享、可长期缓存、与 HTML 并行下载多一次请求
<style> 内联块零请求,适合把首屏关键 CSS 直接嵌进 HTML不随页面复用,改一次要动 HTML
@import组织上像 include被引入的文件要等主 CSS 下载完才开始下载(串行),首屏变慢;生产环境几乎总该换成 link
style="" 属性能被 JS 单点修改、权重最高无法复用、写不了伪类、难以维护
示例 01:三种引入方式与层叠起点在新标签页打开

一条声明怎么胜出

浏览器对每个元素的每个属性只保留一条获胜声明,裁判顺序是:

  1. 重要性与来源:用户带 !important 的样式 > 作者带 !important 的 > 作者普通 > 用户普通 > 浏览器默认。
  2. 层(@layer):未分层的声明,优先级高于所有分层的声明(第十六章)。
  3. 权重(specificity):id / class / 元素三个数量级比较(第四章)。
  4. 书写顺序:前三步比不出结果时,写在后面的赢。

「后写的覆盖先写的」只在权重相同的前提下成立——这是新手最容易记错的一条。

二、语法与书写规范

规则的结构:选择器 + 声明块

/*  选择器(命中谁)  */
h1, .title {
  color: #24292f;      /* 声明 = 属性 + 冒号 + 值 + 分号 */
  line-height: 1.3;
}
  • 逗号是「或」:h1, .title { } 等于两条规则合并。
  • 最后一条的分号可以省略,但别省:追加声明、被压缩工具合并时会出错。
  • 花括号内一条声明缺分号会吞掉下一条;少一个花括号,破坏面能扩到整段样式表。
示例 02:语法结构、简写与命名约定在新标签页打开

注释与分组

CSS 只有 /* … */ 一种注释,// 是 Sass/Less 的能力。错误恢复是整块规则级别的:一个选择器解析失败,浏览器会丢弃直到下一个 } 为止的全部内容,所以一个拼错的选择器影响的往往不止它自己。

声明简写有个必须知道的隐藏行为:简写会把没提到的分量重置为初始值。写了 background: red 之后再单写 background-position,位置可能已经被前一条清掉了——这是「明明写了却不生效」的高频原因之一。

margin: 10px 20px;               /* 上下 10,左右 20(顺序:上 右 下 左,顺时针) */
border: 1px solid #d0d7de;       /* 宽度 样式 颜色 */
background: #f6f8fa url(a.png) no-repeat center / cover;  /* 一行写完多个分量 */

命名约定:BEM 风格

本站所有组件用这一套:

.chip            { }  /* 块:组件本体 */
.chip__count     { }  /* 元素:块内部的组成部分,双下划线 */
.chip--primary   { }  /* 修饰符:同一种东西的变体,双连字符 */
.chip.is-disabled{ }  /* 状态:由 JS 加上的类,配 is-/has- 前缀 */

BEM 不是标准,而是一种能防止选择器膨胀的命名纪律。三条收益:看类名就知道归属、每条规则能单独读懂、权重始终是「单个 class」因此永远低位、方便覆盖。命名带用途而不是带外观:is-error 好于 red-text。

常见语法错误与排查

写法后果
colo: red(属性拼错)该声明被忽略,块内其余照常
color red;(漏冒号)该声明被忽略
margin: 10px 20px(漏分号)下一条声明被吞
少一个 }直到下一个 } 的整块内容失效
width: 100(长度漏单位)除 0 和单位无关关键字外,长度必须带单位
中文全角分号 ;、全角冒号 :中文输入法下的第一大坑,症状同上

排查只用 DevTools 的 Styles 面板:被划掉的规则说明被更高权重或 !important 压制(面板会标出是谁压的),根本没出现的规则说明没命中或整块解析失败。

三、选择器基础

类型、class、id 与通配

p { }        /* 类型选择器:命中所有 p,权重 0,0,1 */
.card { }    /* class:权重 0,1,0 —— 日常主力 */
#header { }  /* id:权重 1,0,0 —— 别拿它写样式(第四章) */
* { }        /* 通配:权重 0,唯一正当用途是重置 */
*, *::before, *::after { box-sizing: border-box; }
示例 03:五组必会选择器在新标签页打开

组合子:空格、>、+ 与 ~

.card p       { }   /* 空格 = 后代,任意深度 */
.card > p     { }   /* > = 子代,只下一层 */
h4 + h4       { }   /* + = 紧邻兄弟,紧跟在后面的那一个 */
p ~ span      { }   /* ~ = 后续兄弟,之后的所有同级 */
A B C > D     { }   /* 混用;从右往左读:D 是 C 的子,C 是 B 的后代 */

选择器从右往左匹配,所以最右侧的「关键选择器」越具体越快。长链 body > div .box ul li a 既慢又脆,DOM 一调整就断——这是重构时最先该删的东西。

属性选择器全家福

语法含义
[href]存在该属性
[href="x"] / [href="x" i]精确等于 / 忽略大小写
[href^="https"]前缀
[href$=".pdf"]后缀
[href*="docs"]包含
[rel~="noopener"]空格分隔的值列表中含某项
[class|="btn"]等于 btn 或以 btn- 开头

它们配合 data-*(HTML 教程第十五章)能实现「数据驱动样式」:JS 只改属性,样式跟着变。

结构伪类

:first-child / :last-child          /* 首尾 */
:nth-child(2n)  /  :nth-child(even) /* 偶数;也可 (2n+1)、odd、from-end(2) */
:nth-child(-n+3)                    /* 前 3 个 */
:only-child                         /* 独子 */
:empty                              /* 无子节点也无文本(空白符也算文本,格式化容易破坏它) */
:not(.is-done)                      /* 排除 */

伪类与伪元素的分界

:hover、:focus、:focus-visible、:active 描述状态(单冒号);::before、::after、::placeholder、::selection 创建 DOM 里不存在的额外内容(双冒号,第十七章展开)。历史单冒号写法(:before)仍兼容,新代码请统一双冒号——这样一眼能看出这条规则到底有没有真的往页面里加东西。

四、层叠、优先级与继承

权重计算:三个数字

成分来自记为
id 的个数#buy1,0,0
class / 属性 / 伪类的个数.btn、[type=text]、:hover、:not() 的参数0,1,0
类型与伪元素的个数p、::before0,0,1
示例 04:权重、!important 与继承在新标签页打开

三个常见误解:

  1. 权重不是十进制,不会进位:11 个 class(0,11,0)也压不过 1 个 id(1,0,0)。
  2. * 和组合子(> + ~ 空格)权重为 0;:where() 权重为 0,而 :is() 取参数里最高的那个。
  3. 元素的 style="" 不参与这套计数,它高于所有规则(带 !important 的声明除外)。

!important 的代价

用了它,唯一的反制手段是「更具体的选择器 + 另一个 !important」。项目里一旦开始出现「为了盖住一行 !important 再加一行 !important」,样式表就已经失控。合理用法只有三类:第三方样式表的临时补丁、工具类(如 .visually-hidden)、用户自己写的无障碍样式表。

顺带一条:!important 只影响这一条声明,但简写的每个分量都会变得重要——margin: 0 !important 同时锁死四个方向。

继承与四个关键字

只有「与文字相关」的大多数属性会继承:

继承不继承
color、font-*、line-height、text-*、letter-spacing、visibility、cursor、writing-mode盒模型(margin/padding/border/width/height)、background、display、position 与定位、flex/grid 全部、overflow、opacity、transform

四个随时手动改变继承行为的关键字:

color: inherit;   /* 强制继承父元素的值 */
color: initial;   /* 强制回到该属性的初始值(切断继承) */
color: unset;     /* 可继承属性 = inherit,不可继承属性 = initial */
color: revert;    /* 回到上一层来源的值,通常是浏览器默认样式表 */

revert 与 unset 的区别很实用:浏览器给 ul 的默认 padding-inline-start: 40px,写 unset 得 0,写 revert 也回到默认——但当你要「撤销用户样式覆盖」或恢复用户代理样式时,只有 revert 语义正确。另有 revert-layer,用于撤销当前 @layer 内的结果(第十六章)。

后写的为什么没生效:四条排查思路

  1. Styles 面板看那条规则有没有被划掉,划掉会附带「被谁压制」的提示。
  2. 规则根本没出现在面板里 → 选择器没命中,或整块解析失败。
  3. 用 Computed 面板搜属性名,能看到最终值来自哪一条声明。
  4. 怀疑是继承:给元素显式写 color: initial,值变了说明原先那个值是继承来的。

五、单位与颜色

绝对单位与相对单位

px 是屏幕上最稳的长度单位(1px = 一个设备像素,高倍屏由浏览器处理)。cm/in/pt 属于打印样式世界,别拿来做网页布局。

单位相对什么典型用途
rem根元素字号字号与间距的首选:改根字号即全站等比缩放
em当前元素的字号(继承类属性看父元素)组件内部尺寸随自身字号走,例如按钮内边距
%视属性而定:宽度对父宽、高度对父高、padding 对父元素宽度、字号对父字号栅格、流体容器
vw / vh视口宽 / 高整屏高度建议用 svh(小视口),避开手机地址栏收起造成的跳动
ch「0」字符宽度限制正文行长(一屏 45–75 字符最易读)
ic一个「水」字宽度中文排版很直观:max-width: 38ic ≈ 一行 38 字
frGrid 的剩余空间只出现在 grid-template-*(第十一章)
示例 05a:rem / em / % / vw / ch 的差异在新标签页打开

还有几类不需要长度单位:line-height: 1.6(无单位数字 = 字号倍数,比 1.6em 安全,不会随继承重复相乘)、margin: 0(0 不需要单位)、flex-grow、z-index、opacity、font-weight。角度有 deg/turn/rad(转 90° 写 .25turn 比 90deg 更直观),时间有 s/ms。

颜色的六种写法

#0969da             /* RRGGBB */
#0969da80           /* RRGGBBAA:最后两位是透明度,00 全透明 */
#069                /* 三位缩写 = #006699,只有每对数字相同才能缩写;#069da(5 位)非法 */
rgb(9 105 218)      /* 现代语法用空格分隔,逗号写法同样支持 */
rgb(9 105 218 / .5) /* 透明度单独给 */
hsl(212 92% 45% / 50%)
hwb(212 4% 15%)     /* 白量、黑量,比 hsl 更符合「调深浅」的直觉 */
oklch(52% 0.19 253) /* 感知均匀 */
royalblue           /* 147 个 CSS 命名色之一,正式项目不建议 */
transparent  currentColor  inherit
示例 05b:颜色写法与透明度在新标签页打开

选写法的依据:固定品牌色用 #RRGGBB;要成组调明暗、生成色阶用 hsl() 或 hwb()(改一个数字出一系列);JS 计算出的色值用 rgb(… / a);做色阶系统用 oklch()——HSL 的「50% 亮度」在不同色相下视觉明度差很多(黄色亮、蓝紫暗),OKLCH 是感知均匀的,同一 L 值的各色看起来真的同样亮。

透明度、currentColor 与暗色主题

alpha 与 opacity 不是一件事:background: rgb(9 105 218 / .5) 只让这一层背景透明,文字仍然清晰;opacity: .5 把整层(含子元素、边框)一起透明,并且会创建层叠上下文(第十二章)。

currentColor 取当前元素的 color 值,于是 border-color: currentColor、box-shadow: 0 0 0 3px currentColor 会随主题自动跟随,不必额外维护变量。

暗色主题的正解是把颜色收进自定义属性(第十四章):

:root { --c-bg: #fff; --c-text: #24292f; --c-primary: #0969da; }
html[data-theme='dark'] { --c-bg: #0d1117; --c-text: #e6edf3; --c-primary: #6aa8ff; }

不要把颜色简单取反:深蓝底配高饱和品牌色会「发光」刺眼,通常要降饱和、提明度。另外 color-scheme: light dark 这一行能让浏览器的默认控件、滚动条、表单底色一起切换——成本极低,收益明显(本站全站样式里就有这一条)。

六、盒模型:所有尺寸问题最后都回到这四层

前面五章解决的是「谁生效」。从这一章起解决「生效成多大」,而元素的大小由盒模型(box model)决定:内容盒(content)→ 内边距(padding)→ 边框(border)→ 外边距(margin),由内向外四层。

示例 06:两种盒模型、边距塌陷与负 margin在新标签页打开

content-box 与 border-box:width 到底指哪一层

浏览器默认是 content-box:width 只描述内容区,padding 和 border 在它外面往外撑。于是这个经典事故:

.card { width: 100%; padding: 16px; border: 1px solid #ddd; }  /* 实际占位 = 100% + 34px,横向溢出 */

改成 border-box 之后,width 变成「最终占位宽」,padding 和 border 从里面扣,width: 100% 就真的是 100%。所以现代项目的第一行几乎都写成:

*, *::before, *::after { box-sizing: border-box; }

选择器里的 *::before, *::after 不能省:伪元素同样是盒子,用 * 选不到它们,很多「加了装饰就溢出」的问题出在这。

两条边界情况值得提前知道:

  • border-box 下 width 不再等于内容宽。文字类容器要保底,用 min-width;
  • 全局 box-sizing: border-box 之后,某些第三方 CSS(老式表单样式、部分日历组件)会算错尺寸,这时对那几个选择器单独写回 box-sizing: content-box,不要反过来取消全局声明。

margin 与 padding:一个占外面、一个占里面

判断标准很简单:背景色要不要覆盖这块区域。要覆盖(内文离边缘的距离、可点击区域的大小)就用 padding;只是把邻居推开就用 margin。

由此引出几条实用的差别:

  • padding 会扩大背景与点击热区,导航项的可点击范围不足时,先加 padding 而不是加 margin;
  • padding 不吃百分比的高度语义:padding: 5% 的上下部分也是按父元素宽度算的(历史上为了做等高列),这条最容易记错;
  • margin 有 auto 这个特殊值:margin-inline: auto 让块级元素在父容器里水平居中,这是「不定宽容器居中」的最短写法。

外边距塌陷:16px + 16px 为什么只有 16px

两个相邻块级元素的垂直 margin 会合并成一个,取绝对值较大的那个。所以「上下相邻的两个卡片各写 margin: 16px」得到的间距是 16px,不是 32px。合并规则有三种情形:正正取大、负负取绝对值大的、正负相加。

更常见的是第二种塌陷——父与子的 margin 穿透:父元素没有 padding-top、没有 border-top、且不是 BFC 时,子的 margin-top 会把父元素一起推下去,看起来像「margin 加到了父身上」。老代码里给父容器随手写 overflow: hidden 就是在处理这个问题(它在创建 BFC)。

今天的最优解不是背塌陷规则,而是改用 Flex/Grid 的 gap:Flex 与 Grid 的子项之间不发生 margin 合并,间距由 gap 一处声明,首尾不会多出空白,也不需要 :last-child { margin: 0 }。这一条能消掉大部分「间距玄学」。

负 margin:合法但危险

margin-left: -12px 能把元素往左拽回 12px,头像列表相互叠压、把内容对齐到视觉基线(数字悬挂在标点左侧)都会用到。风险是它把元素推到容器之外,窄屏会直接产生横向滚动条。

.avatars img { margin-left: -8px; }             /* 叠压效果 */
.avatars img:first-child { margin-left: 0; }    /* 第一个不叠,否则整体左移 */
.avatars img { border: 2px solid #fff; }        /* 必须给描边,否则叠在一起看不出层次 */

只在一维上用负 margin。两列布局用负 margin 拼间距是二十年前的做法(float + 负 margin 的栅格),今天写 gap 或 minmax()。

自查清单

排「尺寸不对」时按这个顺序看:

  1. Computed 面板的盒子图,确认 box-sizing 生效(被 reset 文件覆盖是常见事故);
  2. width + padding + border 是否超过父容器;
  3. 上下间距不对时,先确认有没有塌陷(把 margin 换成 gap 或 padding,如果问题消失,就是塌陷);
  4. 内容把容器撑爆时,查 min-width/min-height(Flex/Grid 子项的自动最小尺寸,见第十章与十一章)。

七、文本与字体:中文排版的三个特殊点

文字是网页的主要内容,而 CSS 里跟文字有关的属性分两组:一组决定用什么样的字(font-*),一组决定这些字怎么排(text-*、white-space、word-break)。

示例 07:字体栈、行高、换行与截断在新标签页打开

字体栈与 fallback 顺序

body {
  font-family: system-ui, -apple-system, "Segoe UI", "PingFang SC",
               "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}

font-family 是一个候选列表,浏览器从左往右取第一个「本机装有」的字体,全部落空才用最后的通用族关键字。因此三条规则:

  • 带空格的名字必须加引号("PingFang SC"),关键字(sans-serif、system-ui、monospace)不加;
  • 末尾一定要放通用族兜底;
  • 中文站一定要写中文族名。只写 Arial 时中文交给浏览器随机 fallback,Windows 与 macOS 的字重表现差别很大,bold 在一种系统上偏细、在另一种上糊成一团。

system-ui 取操作系统界面字体,优点是零下载、风格与用户设备一致;缺点是各平台长得不一样,做品牌感强的标题时通常换成西文品牌字体 + 中文族名的组合。

网络字体用 @font-face 声明,配 font-display: swap(先回退字显示、下载完再换),否则首屏会出现一段纯白文字(FOIT)。中文字体动辄几 MB,务必只加载需要的字重,或按章节做子集化。

font-size、font-weight、line-height

  • 字号用 rem:根字号是用户/浏览器设置的入口,全站用 rem 时这一设置才真的起作用。正文不小于 14px,可点击文字建议 16px。
  • 字重优先用数字(400/600/700),别依赖 bold 关键字:bold 固定映射到 700,而中文字体常常只有 400 与 700 两档,浏览器会合成加粗(人为描边),中文一多就糊。中文正文里 font-weight: 600 往往比 bold 更耐看。
  • 行高写无单位数字:line-height: 1.7 相对本元素字号计算且继承的是这个数字;写成 1.7em 时子元素会按自己的字号再乘一次,嵌套层级一深就翻倍。中文正文建议 1.6–1.8,标题 1.2–1.3。

font 简写顺序固定:font: style variant weight size/line-height family,size 与 family 必填,缺省时其余部分会被重置为初始值——这也是为什么「只想要 font: 16px sans-serif 而不重置行高」是错的写法。

字距、词距与大小写

letter-spacing 用 em 为单位才随字号变化:大标题需要负字距(-0.02em)才不显松散,全大写的小标签需要正字距(.08em–.12em)。word-spacing 只影响词间空白,中文几乎没有词,主要作用于西文。

text-transform: uppercase 只改显示、不改 DOM 文本,因此对搜索、复制、读屏都可能造成偏差;中文还有 full-width 值。做小标签时用它,但正文内容不要用(英语句首大写规则会丢失)。

对齐与装饰

text-align 管行内内容在块里的对齐:left/right/center/justify。justify 在西文里要配 hyphens: auto 和正确的 lang,否则词间空隙会夸张到出现「河流」;中文用 justify 效果接近两端对齐但没有连字符机制。text-align-last 单独控制最后一行。

text-decoration 家族需要注意它是画在文字上而非盒子下:

a { text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; }

text-underline-offset 让下划线离开基线,中文尤其需要(汉字下沿低,默认位置容易贴着笔画)。删除上下划线时写 text-decoration-line: none 而不是 text-decoration: none,后者会重置整族属性。

white-space、断行与截断

这一组属性治的是「文字撑破容器」:

  • white-space: nowrap 禁止折行(表格单元格里让数字成行最方便);
  • white-space: pre-wrap 保留用户输入的换行和空格(评论区必需);
  • overflow-wrap: anywhere 让超长单词(长 URL、base64)强制拆断;
  • word-break: break-all 允许在任意字符间断行,中英混排有时需要,但纯西文会拆得很丑;keep-all 禁止中文断行(避免句首出现孤立标点时可用 line-break: strict);
  • hyphens: auto 需要元素或祖先有正确的 lang。

单行截断是固定的三件套:

.truncate { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

多行截断目前仍是带前缀的写法(但所有主流内核都支持,属事实标准):

.clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

被截断的文字仍在 DOM 里:读屏会念全文,用户选中复制也能拿到全文,但看不见的那部分等于不存在——关键信息不要靠截断承载,配合 title 属性或「展开」按钮。

中文排版的三个特殊点

  1. 标点悬挂(hanging-punctuation)与行首禁止字符(line-break: strict)能消掉中文段落「行首一个逗号」的难看;Safari 支持较好,Chrome 一线在演进,写它属于渐进增强。
  2. 行高与视觉密度:汉字是方块字,没有西文的上伸下延,同样行高下中文显得更密。中文段落行高比同字号西文再加大约 0.1–0.2 才舒服。
  3. 行长用 ch 或 ic 限制:西文正文一行 45–75 字符最易读,max-width: 65ch;中文可以用 max-width: 38ic(ic = 一个「水」字的宽度)达到同样效果。

八、背景、边框与阴影:视觉层次靠这三样

示例 08:background 各轴、多层背景、圆角与阴影在新标签页打开

background-image 与 background-size

背景的多条轴各自独立:image(图或渐变)、position、size、repeat、origin、clip、attachment。最常配的是封面图三件套:

.hero { background-image: url(hero.jpg); background-size: cover; background-position: center; }

cover 铺满且不变形,代价是可能裁掉内容;contain 完整显示,代价是可能留白。人物照片做背景时 cover 常把脸裁掉,用 object-fit + <img>(第十三章)比背景图更好控制,因为可以用 object-position 精确指定保留区域,且图片能被 alt 与懒加载覆盖。

background-position 的百分比语义容易记错:50% 50% 是「图片的 50% 处对齐容器的 50% 处」,不是「距左 50%」。

background-origin/background-clip 决定绘制从哪算起、画到哪结束。最常用的是文字裁切:

.gradient-text {
  background: linear-gradient(90deg, #0969da, #8250df);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

(background-clip: text 现在主流内核都支持,但必须保证文字有回退色,且它会让真实 color 失效——不要对正文大范围使用,装饰标题为宜。)

多层背景:先写的在上面

.card-hero {
  background-image:
    radial-gradient(circle at 20% 30%, rgba(255,255,255,.35), transparent 40%),
    repeating-linear-gradient(90deg, rgba(255,255,255,.12) 0 6px, transparent 6px 14px),
    linear-gradient(135deg, #0969da, #8250df);
  background-size: auto, auto, 100% 100%;
}

逗号分隔的每一层对应一组值;某一层少写的值会往回取该属性的第一个值(所以只写一个 background-position 时所有层共用它)。层与层之间可以用 background-blend-mode 指定混合方式。这个「一层高光 + 一层纹理 + 一层底色」的结构,就是不用图片做出有质感封面的标准配方。

渐变:它是 <image>,不是颜色

linear-gradient()、radial-gradient()、conic-gradient()(第十九章细讲)都能出现在任何接受图片值的地方。两个高频技巧:

/* 硬切色带:同一位置写两次色标 */
.progress { background: linear-gradient(90deg, var(--c-primary) 62%, #eaeef2 62%); }
/* 斜纹/斑马线:repeating-linear-gradient */
.zebra { background: repeating-linear-gradient(45deg, #fff8c5 0 10px, transparent 10px 20px); }

边框:宽度、样式、圆角

border 是盒子的一部分(border-box 下从宽度里扣),改宽度会挤动内容。四条规则:

.card:hover { border-color: #0969da; }        /* 只换色不换宽:避免 hover 时内容跳动 */
.card { border: 1px solid #d0d7de; border-left: 4px solid #0969da; }  /* 左侧强调条 */
.pill { border-radius: 999px; }               /* 胶囊 */
.leaf { border-radius: 12px 4px 12px 4px; }   /* 四角不同:左上 右上 右下 左下 */

border-radius 可以写 12px / 24px(水平/垂直半径不同)做出椭圆角;border-radius: 50% 得到圆(元素必须是正方形,否则是椭圆)。圆形头像更稳的做法是图片自身保持正方形 + object-fit: cover + 圆角。

边框想「不占空间」时用 outline:它在盒子外绘制、不影响布局,但保持矩形(新内核已跟随圆角)。所以焦点环用 outline,装饰用 box-shadow。

box-shadow 的四个可省参数

语法 inset? offset-x offset-y blur? spread? color。模糊半径为 0 时得到硬阴影,做复古/新粗野风格;inset 得到内阴影,可当描边;一个选择器里可以叠多层:

.tile { box-shadow: inset 0 0 0 1px #d0d7de; }              /* 不占布局的 1px 描边 */
.pop  { box-shadow: 0 1px 3px rgba(0,0,0,.12), 0 8px 24px rgba(0,0,0,.14); }  /* 近处锐利 + 远处扩散 */

两层阴影是「像真的」的关键:只有一层大模糊的阴影看起来发灰,像蒙了雾。同理,暗色主题下阴影几乎看不见,应改用更亮的边框表达层次,而不是加深阴影。

性能:box-shadow 的动画代价高(要重绘),需要动效时改为「阴影写在伪元素上、动它的 opacity」(第十八章)。

焦点可见性不是装饰,是合规

:focus-visible { outline: 3px solid var(--c-primary); outline-offset: 2px; }

outline: none 单独出现就是可访问性缺陷;要么保留默认焦点环,要么换一条足够明显的替代样式。:focus-visible 与 :focus 的区别(只在键盘导航时显示焦点环)见第十五章。

九、display、可见性与 float:历史与今天的分工

示例 09:块级行内、三种隐藏与 float 清除在新标签页打开

block / inline / inline-block

display 决定元素参与哪一种布局算法,而不是「是否可见」。三种最基础的:

常见标签宽高垂直 margin独占一行
blockdiv p section li生效生效(会塌陷)是
inlinea span b code img input无效无效否
inline-block按钮、徽章、图标生效生效(不塌陷)否

行内元素「设了宽高没反应」是初学者最常见的困惑,正确答案通常是把它变成 inline-block,或者更好的是——给父级写 display: flex(第十章),让子项自动获得块级尺寸能力。

inline-block 之间那道缝来自 HTML 源码里的换行符被当作空白渲染。font-size: 0、写成一行、或者用 Flex 都能消掉,其中用 Flex 是唯一没有副作用的。

三种「看不见」的代价完全不同

写法是否占位是否可交互读屏能否念到能否做过渡
display: none不占否否(移出可访问性树)否
visibility: hidden占位否否可(离散切换)
opacity: 0占位是(仍可点,注意误触)是是(首选动画属性)

选择依据:

  • 需要淡入淡出 → opacity 配 visibility,动画结束后再用 visibility: hidden 彻底移出:
    .popover { opacity: 0; visibility: hidden; transition: opacity .18s ease, visibility .18s; }
    .popover.is-open { opacity: 1; visibility: visible; }
    
  • 只是不想出现在布局里 → display: none,但它会打断过渡;
  • 视觉隐藏但对读屏保留(例如「已选中」状态、图例说明)用 .sr-only 那套绝对定位 + clip-path 的写法,不要用 opacity: 0(会被读出但也可被点击)或 display: none(读不到)。

float:它唯一被设计出来的用途是文字环绕

float 让元素脱离正常流并贴到一侧,后续行内内容绕着它排——这是报纸版面需求。今天仍然该用 float 的只有一种场景:图文环绕(首图在段落左上,文字绕排)。Flex/Grid 做不到绕排。

副作用则是所有人都熟悉的「父容器高度塌陷」:浮动元素不撑高父级,父级后面的内容会被压上来。四种清除方式:

.clearfix::after { content: ""; display: block; clear: both; }  /* 传统 */
.parent { overflow: hidden; }    /* 顺带创建 BFC,但会裁掉溢出内容 */
.parent { display: flow-root; }  /* 语义最准确、无副作用的现代写法 */

两列/三列布局、等高卡片、导航条——这些历史上用 float + 负 margin 完成的事情,现在都归 Flex 与 Grid。float 还剩一个合理去处是 HTML 邮件(客户端 CSS 支持极差)。

BFC 顺手解决边距塌陷

块级格式化上下文(BFC)是一个「内部布局与外界隔离」的盒子。它不影响外部 margin 塌陷,但父容器一旦成为 BFC,子的 margin-top 就不再穿透。触发条件(任一即可):float、position: absolute/fixed、display: flow-root/inline-block/table-cell/flex/grid、overflow 非 visible、contain: paint。

不用背——遇到「margin 莫名跑到父元素上」时,给父容器加 display: flow-root 即可。

其余 display 值速查

grid / flex 是布局算法(第十、十一章);inline-flex、inline-grid 是它们的行内版本;table / table-cell 仍是不定高内容垂直居中最省事的办法(虽然语义上应该用 Flex);contents 让盒子自身「消失」、子元素直接参与父级布局,方便但会丢掉该元素自身的背景与边框,并曾引发展示与可访问性问题,谨慎使用。列表 ul 上写 list-style: none 会让 Safari/VoiceOver 不再播报列表语义,需要用 role="list" 补回(这条属于 HTML 教程第十七章的可访问性清单)。

十、Flexbox:从主轴到实战

一维布局(一行或一列)交给 Flex。它的模型只有一个概念需要真正理解:主轴与交叉轴,而这两根轴会随 flex-direction 旋转——这也是 Flex 最大的困惑来源。

示例 10a:flex-direction、justify/align 与 gap在新标签页打开

主轴与交叉轴:属性名分两套

justify-* 永远管主轴,align-* 永远管交叉轴。direction: row 时 justify 是水平、align 是垂直;改成 column 后两者表现整体旋转 90°。判断方法只有一个:先确认主轴方向,再决定该写 justify 还是 align。

flex-direction 四个值:row(默认)、row-reverse、column、column-reverse。*-reverse 只反转视觉顺序,DOM 顺序、Tab 焦点顺序、读屏顺序都不变——这会造成「看到的顺序与听到的顺序不一致」,属于可访问性风险,能用 DOM 顺序解决就别反转。

justify-content 的五个常用值只有在容器有剩余空间时才有区别:flex-start、center、flex-end、space-between(两端贴边)、space-around / space-evenly(首尾也留空隙,后者所有空隙严格相等)。

align-items 有 stretch(默认,需子项不设高度)、flex-start、center、flex-end、baseline。字号不同的两项并排时 baseline 比 center 更整齐。单个子项要叛逆时用 align-self。

gap:容器间距的正确写法

.list { display: flex; gap: 12px 20px; }   /* 行间距 列间距 */

gap 只加在相邻项之间,首尾不多出空白,因此从此不需要 :last-child { margin-right: 0 }。子项换行时(flex-wrap)行与行之间的间距同样由它管。Flex、Grid、多列 column-* 都支持 gap。

flex-grow / flex-shrink / flex-basis

示例 10b:flex 简写、wrap、order 与 min-width 溢出坑在新标签页打开

三个属性分别管三件事:

  • flex-grow:容器有剩余空间时按份数分给谁。分的是剩余空间,不是总宽度——所以两个 grow: 1 的项如果内容宽度不同,最终宽度也不相等。
  • flex-shrink:容器不够宽时谁让步(默认 1)。实际收缩量按 shrink × basis 加权,不是按数字直接分。不想被压的(图标、关闭按钮、固定侧栏)写 flex-shrink: 0。
  • flex-basis:伸缩的起点尺寸,优先级高于 width(在主轴方向上)。

flex 简写有四个必须记住的展开:

写法展开意图
flex: 11 1 0%严格等宽(basis 是 0,忽略内容宽度)
flex: auto1 1 auto按内容比例分配剩余,能长能缩
flex: initial0 1 auto默认值:不生长,但会被压缩
flex: none0 0 auto完全固定

「等宽但内容长的那一项溢出」通常是误用了 flex: auto;「固定 240px 侧栏」的正确写法是 flex: 0 0 240px(或 flex-basis: 240px; flex-shrink: 0),只写 width: 240px 会被压缩。

flex-wrap 与 order

flex-wrap: wrap 让子项放不下时换行,配合 flex-basis 就是最省事的响应式标签云/工具条:

.tags { display: flex; flex-wrap: wrap; gap: 8px; }

order 只改视觉顺序(默认 0,可负),典型用途是「DOM 按语义写、移动端把主内容提到前面」。风险与 *-reverse 相同:Tab 顺序跟着 DOM 走,键盘用户会跳来跳去。

最经典的坑:min-width 不是 0

Flex 子项的自动最小尺寸是 min-content,因此子项无法被压缩到内容宽度以下。这直接导致「text-overflow: ellipsis 在 flex 子项里不生效」这个高频问题:

.row { display: flex; }
.row .side { flex-shrink: 0; }        /* 图标/按钮不让步 */
.row .text { min-width: 0; }          /* 关键:允许收缩到内容以下,省略号才生效 */
.row .text span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

同一类问题在嵌套 Flex、Grid 子项(1fr 的下限是 auto)里都会出现,写 minmax(0, 1fr) 或 min-width: 0。纵向(column)时对应的是 min-height: 0,否则子项会撑破滚动容器。

三个高频实战配方

/* 居中:完全居中一行 */  .c { display: flex; align-items: center; justify-content: center; }
/* 工具条:左标题右操作 */ .bar { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
/* 吸底页脚 */            body { min-height: 100svh; display: flex; flex-direction: column; } main { flex: 1; }

等高卡片靠 align-items: stretch(Flex 的默认值)自动实现,卡片内部把页脚写成 margin-top: auto 或给它 flex: 1 吃掉中间空白,就能「底部对齐」。这是 Flex 比 float 简单得多的地方。

十一、Grid:从网格轨道到自适应布局

Flex 解决「一行里怎么排」,Grid 解决「整页分成几格」。Grid 是二维的:可以先声明列,再声明行,然后让子项落位。

示例 11a:轨道、fr、命名区域与对齐在新标签页打开

grid-template-columns 与 fr

.g { display: grid; grid-template-columns: 200px 1fr 1fr; gap: 16px; }

fr 是剩余空间的等份,只在 grid-template-* 里可用(写成 width: 1fr 无效)。2fr 1fr 是二比一,但比例算的是扣掉固定轨道与 gap 之后的剩余空间,所以固定列宽变化时比例看起来会变,这是正常的。

repeat(12, 1fr) 是简写;repeat(3, minmax(0, 1fr)) 是防御性写法(理由见上一章的 min-content 问题);grid-template-columns: [侧栏] 240px [主区] 1fr 给轨道命名,子项就能写 grid-column: 侧栏。

行也一样:grid-template-rows 描述你写明的轨道,超出的部分进入隐式轨道,尺寸由 grid-auto-rows 决定(默认 auto),排布方向由 grid-auto-flow(row / column / dense)控制。

命名区域:把布局写成 ASCII 图

.page {
  display: grid;
  grid-template-columns: 216px minmax(0, 1fr) 232px;
  grid-template-rows: 48px 1fr 40px;
  grid-template-areas:
    "head head head"
    "nav  main aside"
    "foot foot foot";
}
header { grid-area: head; }
main   { grid-area: main; }

这是 Grid 比 Flex 更适合「整页骨架」的原因:响应式换布局只需重写 grid-template-areas 那几行,HTML 与子项选择器一行不动。约束是区域名组成的矩形必须完整(不能有 L 形空洞),否则整条声明静默失效;空格子用 . 表示。

本站教程详情页的三栏(左目录 / 正文 / 右目录)就是这个思路——只是用 max-width 媒体查询把三栏收成单栏,并让左栏变成横向滚动的 chip 条。

跨行跨列:数的是「线」

示例 11b:span、minmax + auto-fit、subgrid 与 12 栏在新标签页打开

N 条线围成 N-1 个格子,所以「占第 2、3 列」写作 grid-column: 2 / 4。-1 是最后一条线,grid-column: 1 / -1 就是横跨整行——写整行标题、整行页脚最常用。

grid-area 的四值顺序是 行开始 / 列开始 / 行结束 / 列结束(与 margin 的顺时针一样)。grid-column: span 3 让自动放置决定起点,比写死线号更适合响应式(断点变化时线号会全部错位)。

一行代码的响应式:minmax() + auto-fit / auto-fill

.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 16px;
}

auto-fit 与 auto-fill 的差别只在末尾有没有空轨道:auto-fill 保留算出来但没内容的轨道(卡片会显得靠左一小堆),auto-fit 把空轨道压成 0,让现存卡片继续伸展铺满。内容少又要「铺满」用 auto-fit;内容多、希望网格始终对齐用 auto-fill。

min(240px, 100%) 里那个 100% 是防窄屏横向滚动条的:容器比 240px 还窄时,minmax 的下限会强行撑破容器。

这套写法能替掉大量「卡片墙 / 图库 / 相关列表」的媒体查询——按内容自身宽度决定断点,比按设备宽度更稳。

subgrid:让子卡片共享外层轨道

.panel { display: grid; grid-row: span 3; grid-template-rows: subgrid; }

跨的格子不再是「自己的轨道」而是复用父网格的,于是每张卡片的标题行、正文行、按钮行严格对齐——以前要每张卡硬写行高才能做到的事。浏览器基线:subgrid 需要较新的内核(Firefox 71 / Chrome 117 / Safari 16 一线),旧内核会退化成普通 Grid 而不破坏内容;上线前请以 caniuse 当前数据为准,并配 @supports (grid-template-rows: subgrid) 做渐进增强(第二十章)。

Grid 的三套对齐属性

  • justify-items / align-items:格子内部内容在水平/垂直的位置;place-items 是两者简写(display: grid; place-items: center; 是最短的完全居中);
  • justify-content / align-content:整个网格轨道在容器里的分布(有剩余空间才看得出);
  • justify-self / align-self:单个子项叛逆。

Flex 只有 align-items/justify-content 这一套;Grid 因为有两个明确的轴向,同名属性含义不同(Grid 的 justify-* 管水平、align-* 管垂直)。这正是「同一行 CSS 在两种布局下表现不同」的来源,别混着记。

常见坑速查

现象原因与修法
设了 1fr 仍横向溢出1fr 下限是 auto(= min-content)→ 写 minmax(0, 1fr)
子项 height: 100% 不生效用 grid-template-rows 或依赖默认的 align-items: stretch,别猜父高
四周空白不对称四周来自容器 padding,gap 只管轨道之间
auto-fit 把卡片拉得很宽给上限:minmax(240px, 320px),外层再 justify-content: center
子项叠在同一格忘记 gap 或显式写了相同的 grid-area(Grid 允许重叠,做叠层时是特性)

十二、定位与层叠上下文

position 决定元素相对谁偏移;z-index 只在同一个层叠上下文内比大小。这两条理解到位,「弹层被挡住」「absolute 跑到左上角」之类的排错时间能砍掉一半。

示例 12:五种 position、inset、z-index 与 sticky在新标签页打开

五种 position 的差别在参考系

值参考系是否脱离文档流典型用途
static—(top/left 完全无效)否默认
relative自身正常位置否(原空间保留)给子绝对定位当锚点、微调图标
absolute最近的已定位祖先是徽标、下拉、遮罩
fixed视口(祖先有 transform 类属性时改变)是吸底按钮、对话框
sticky滚动容器 + 自身所在区块范围否表头、分区标题、吸附导航

sticky 失效的三个常见原因:没写 top 等偏移值、某个祖先设了 overflow(改变了滚动容器)、父容器高度不足以容纳滑动范围(吸附不了几像素就跟着走了)。top 在有吸顶页头的站点里通常要写成 var(--header-h),否则会被盖住——本站左右目录栏正是用 position: sticky + top 与页头高度联动。

表格 <thead> 也能 sticky,但需要 border-collapse: separate(合并边框时边框不跟随)。

inset 简写与三种绝对居中

inset: 8px 12px auto auto 是 top right bottom left,顺序与 margin 一致。绝对居中三种写法:

/* 一:Grid(推荐) */ .wrap { display: grid; place-items: center; }
/* 二:transform(不要求父级尺寸) */ .m { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
/* 三:inset + auto margin(要求固定宽高) */ .m { position: absolute; inset: 0; margin: auto; width: 120px; height: 40px; }

translate(-50%, -50%) 的百分比相对自身尺寸,这是它能在不知宽高时居中的原因。

z-index:只在自己的层叠上下文里比大小

z-index 只在同一个层叠上下文内有意义。父级一旦形成层叠上下文,子级写 z-index: 9999 也出不去——它只能在那个盒子内部称王,外面一个 z-index: 2 的兄弟就能盖住它。

创建层叠上下文的条件(任一成立):

  • position 非 static 且 z-index 非 auto(最常见的来源);
  • opacity 小于 1;
  • transform / filter / backdrop-filter / perspective / mask 相关属性;
  • will-change 里声明了上述任一属性;
  • isolation: isolate(专门用来创建上下文的属性,语义最干净);
  • position: fixed;
  • Flex / Grid 子项的 z-index 非 auto;
  • contain: paint / content-visibility 等生成独立绘制层的属性。

排错顺序固定:① 从目标元素往上找最近的层叠上下文祖先;② 只在这一层里比 z-index;③ 需要「跳出去」时,要么去掉祖先的 transform/filter,要么把浮层挂到更外层的容器(或用原生 <popover>、position: fixed)。

数值请建自己的刻度而不是 9999:10 下拉、50 遮罩、100 对话框、200 toast。当所有人都写 9999 时,任何一层都赢不了。

顺带区分两件事:层叠上下文(本节的几何/属性问题)与层叠层级(@layer,第十六章的声明顺序问题)名字相近,机制完全无关。

fixed 会被祖先「抢走」

祖先一旦有 transform / filter / perspective / backdrop-filter / will-change / contain: paint,fixed 的参考系就从视口变成那个祖先,表现为「弹窗跟着页面乱跑」。动画里给祖先加 transform 时尤其容易碰上。

实战配方

/* 下拉菜单 */ .dd { position: relative; } .dd-menu { position: absolute; top: calc(100% + 4px); left: 0; }
/* 徽标 */ .badge-wrap { position: relative; } .badge { position: absolute; top: -6px; right: -6px; }
/* 遮罩 */ .mask { position: fixed; inset: 0; background: rgb(0 0 0 / .5); z-index: 50; }

下拉要同时写 :hover 和 :focus-within,否则键盘用户打不开。弹出层靠近视口边缘被裁掉时,用 position: fixed + 定位 API,或直接改用原生 <popover> / anchor()(仍在演进,见 HTML 教程第十三章)。

十三、响应式与流式排版:能连续变化的就别用断点

响应式的目标不是「适配三种手机」,而是任何宽度都不破。做法上分两层:能用连续函数解决的(字号、间距、卡片列数)用流式;真的需要结构切换时(三栏变一栏、导航变汉堡)才用媒体查询。

示例 13a:断点、移动优先与非尺寸类媒体查询在新标签页打开

断点跟着内容走,不跟着机型走

把浏览器从 320px 慢慢拉宽,在哪里变丑就在哪里设断点——通常 2 到 4 个就够。照抄某个框架的 576/768/992/1200 能用,但那些数字与你的内容无关。

单位建议写 rem(min-width: 40rem)而不是 px:用户放大根字号时媒体查询会提前触发,避免文字溢出。

新语法支持范围表达式,读起来更直观:

@media (40rem <= width <= 60rem) { … }   /* 等价于 min-width 与 max-width 的 and 组合 */
@media (min-width: 60rem), (orientation: landscape) { … }   /* 逗号 = or */

移动优先:默认写窄屏,逐级 min-width 增强

.layout { display: grid; gap: 12px; }                        /* 默认:单列,不包在媒体查询里 */
@media (min-width: 40rem) { .layout { grid-template-columns: 1fr 1fr; } }
@media (min-width: 60rem) { .layout { grid-template-columns: 200px 1fr 200px; } }

好处是基础样式就是最小屏样式,逐级叠加;反过来的桌面优先(默认给桌面、逐级 max-width 降级)容易漏掉基础样式,也让覆盖关系变成「后写的规则更弱」,与维护成本相悖。

不只是宽度:这些也属于媒体查询

特性查询用途
prefers-color-scheme: dark跟随系统深浅色(本站用 html[data-theme] 手动切换,见第十四章)
prefers-reduced-motion: reduce关闭/收敛动效,必须提供(第十八章)
hover: hover and pointer: fine只给鼠标设备写 hover 效果,避免触屏「点一下卡在 hover 态」
orientation / aspect-ratio横竖屏、宽高比
display-mode: standalonePWA 已安装态
forced-colors: activeWindows 高对比模式,需要专门准备描边
print打印:隐藏导航、展开折叠内容、break-inside: avoid、给外链补真实地址

全站级降级写法(可接受,但更好的做法是逐条设计静态替代方案):

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}

流式排版:clamp() / min() / max()

示例 13b:clamp 流式字号、aspect-ratio 与 object-fit在新标签页打开

clamp(最小, 理想, 最大) 让数值随视口连续变化:

h1 { font-size: clamp(1.5rem, 1.06rem + 2.2vw, 3rem); }
section { padding-block: clamp(24px, 4vw, 64px); gap: clamp(8px, 1.5vw, 20px); }

理想值必须是带视口单位的表达式才会真的流动(只写单值就退化成 min/max 选择)。斜率换算:(最大px − 最小px) ÷ (最大视口px − 最小视口px) × 100,基准 = 最小px − 斜率 × 最小视口px ÷ 100。

min() / max() 常一行替掉一条媒体查询:

.wrap { width: min(100% - 2rem, 68rem); margin-inline: auto; }   /* 上限 68rem,窄屏自动留边距 */
.gutter { padding-inline: max(1rem, env(safe-area-inset-left)); }  /* 刘海屏安全区 */

aspect-ratio 与 object-fit:图片适配的两个正交属性

aspect-ratio: 16 / 9 让盒子保持比例(取代 padding-top: 56.25% 那套 hack);object-fit 让替换元素的内容在这个盒子里如何被裁:

值行为用途
fill默认,拉伸变形几乎不用
contain完整显示,可能留白logo、截图、图表
cover铺满并裁切等尺寸缩略图、卡片封面
none原尺寸居中裁图标
scale-downnone 与 contain 中取较小的大图不放大

配 object-position: right top 指定裁切时保留哪一侧(人像图常用 center 25% 保住脸)。卡片封面的标准配方就是两者相加:aspect-ratio: 16 / 9 + object-fit: cover——本站教程入口卡片正是这个写法,所以三张 16:9 的封面 SVG 放进去不会被裁掉内容。

还有一条防布局抖动的规则(CLS):<img> 必须写 width/height 属性,配合 img { max-width: 100%; height: auto; },比例由 HTML 属性提供、尺寸由 CSS 限制。

十四、自定义属性与主题化

自定义属性(custom properties,俗称 CSS 变量)是这门课里回报率最高的一个特性:它同时解决「一处改全站」「组件可配置」「暗色主题」「JS 与 CSS 传值」四件事。

示例 14:变量作用域、暗色主题与 JS 读写在新标签页打开

声明与读取:两条语法

:root { --brand-500: #0969da; --radius-3: 10px; --space-3: 16px; }
.btn { background: var(--brand-500); border-radius: var(--radius-3); padding: var(--space-3); }

它与预处理器变量的本质区别:预处理器变量在编译期被替换成常量,自定义属性在渲染期按元素逐个求解。所以它是活的——会继承、会被就近覆盖、能被 JS 在运行时改,也能被媒体查询改写。

命名与作用域

命名建议「类别-语义-刻度」:--text-1、--bg-2、--brand-500、--space-3、--radius-2。反面例子是 --blue 和 --big:这类「描述外观」的名字在换主题时会立刻失效(暗色下那个蓝已经不是你写的蓝),而描述意图的名字只需要改取值。

作用域由声明它的那个选择器决定:

  • 写在 :root → 全站可见;
  • 写在组件类上 → 只有该子树可见;
  • 写在 @media 里 → 响应式变量(本站的 --header-h 就随窄屏变化);
  • 写在元素的内联 style 上 → 只有这一个元素及其后代可见,也是「使用方定制组件」的入口。

var() 的回退值与「无效」行为

.x { color: var(--maybe, blue); }          /* 变量不存在时用回退值 */
.y { padding: var(--a, var(--b, 8px)); }   /* 回退可以嵌套 */
.z { width: calc(var(--w, 100px) * 2); }   /* 回退参与计算 */

变量未定义且没给回退时,整条声明在该元素上被视为无效(相当于回到继承值或初始值),不会报错、不会崩溃——所以「变量拼错」的表现是样式静默丢失,排查要看 Computed 面板。

一个必须知道的特例:用 @property 注册过的变量有「guaranteed-invalid」之外的初值语义,值为空时行为与未定义不同;未注册的变量请一律给回退值。

主题化:只改变量,一条规则都不动

:root { --bg-1: #fff; --text-1: #24292f; --brand-500: #0969da; color-scheme: light; }
html[data-theme='dark'] { --bg-1: #0d1117; --text-1: #e6edf3; --brand-500: #6aa8ff; color-scheme: dark; }

三件配套事项:

  1. 写 color-scheme:一行让浏览器的默认控件、滚动条、表单底色、高亮色一起切换,成本极低;
  2. 暗色下品牌色要重新调:降饱和、提明度(#0969da → #6aa8ff)。深蓝底配高饱和原色会「发光」刺眼;简单取反(黑底白字)也不是好主题;
  3. 切换与持久化:JS 改 <html> 的 data-theme,把选择存进 localStorage,并在首屏绘制前用一段内联脚本应用,否则会闪一下浅色(FOUC)。

本站就是这一套:main.css 里 html[data-theme='dark'] 覆盖同一批变量名,所有组件的规则完全不用改。

如果你更希望跟随系统而不是手动切换,把选择器换成 @media (prefers-color-scheme: dark)(第十三章);两者可以共存:先按系统给默认值,再用 data-theme 让用户覆盖。

组件级变量:把变量当「公共 API」

.badge { --badge-bg: #eaeef2; --badge-fg: #24292f; background: var(--badge-bg); color: var(--badge-fg); }

使用方不需要新增类名、不需要 !important,只要给祖先或自身设变量就能定制:

<span class="badge" style="--badge-bg:#ddf4ff">定制色</span>
<div class="zone"><!-- 这里的 badge 全部换色 --></div>

这比「写十个修饰类」更可组合,尤其适合图表、卡片、主题色块一类需要按数据着色的组件。

用变量驱动计算与响应式

.grid-auto { --cols: 3; grid-template-columns: repeat(var(--cols), 1fr); gap: var(--space-3); }
.bar { --pct: 62%; } .bar > i { width: var(--pct); }
@media (max-width: 40rem) { :root { --cols: 1; --space-3: 12px; --header-h: 96px; } }

把「断点决定的一组尺寸」集中成变量,比在每个规则里重复写媒体查询干净得多——这也是本站把吸顶高度写成 --header-h 并在滚动判定里读取的原因。

用 JS 读写变量

getComputedStyle(el).getPropertyValue('--brand-500').trim();   // 读解析后的值
el.style.setProperty('--pct', '80%');                          // 写(只影响这棵子树)
el.style.removeProperty('--pct');                              // 删内联值,落回样式表

反过来「JS 把状态传给 CSS」也用它,比切一堆类名更可组合:

<div style="--index: 3">…</div>
.item { transition-delay: calc(var(--index) * 60ms); }

一个限制:变量本身不能被平滑过渡(浏览器不知道该按什么规则插值)。要动画变量,先用 @property 注册语法类型:

@property --pct { syntax: "<percentage>"; inherits: false; initial-value: 0%; }
.bar > i { width: var(--pct); transition: --pct .6s ease; }   /* 现在可动画了 */

浏览器基线:@property 在 Chrome 85 / Safari 16.4 / Firefox 128 一线支持,属渐进增强能力,不支持时过渡静默退化为跳变,布局不受影响;落笔前请以 caniuse 当前数据为准。

常见误区速查

误区事实
变量是常量按元素逐个求解,会继承、会被就近覆盖
值只能是颜色或长度可以是任意 token 序列,包括整串阴影值
calc() 里可以随便乘变量长度计算要求带单位,纯数字变量要写成 calc(var(--n) * 1px)
循环引用会崩相关声明静默无效(不报错,但很难查)
变量性能差解析成本可忽略,别为了「少几个变量」放弃主题化

十五、现代选择器与函数:少写权重、少写类名

这一章的选择器都不是「锦上添花」:它们各自替掉了一类以前必须靠 JS 或靠加权重才能完成的需求。

示例 15::is()/:where()/:has()、nth-child of S 与 calc() 混合单位在新标签页打开

:is() 与 :where():把选择器体积收敛掉

/* 展开写法:4 个前缀 × 3 个后缀 = 12 条 */
.card .title, .card .meta, .panel .title, .panel .meta { … }
/* 收敛写法:一条 */
:is(.card, .panel) :is(.title, .meta) { … }

两者唯一的但关键的差别是权重:

  • :is(A, B) 的权重 = 参数里最高的那个;
  • :where(A, B) 的权重 = 0。

所以给组件写「默认样式」时用 :where()(使用方随手就能覆盖,不需要 !important),需要强调时用 :is()。

解析风险必须知道::is() / :where() 参数里出现浏览器无法解析的选择器片段,会导致整条规则被丢弃。因此不要把「很新的选择器」塞进它们的参数里当降级方案——那种情况请用 @supports(第二十章)或独立规则。

:has():真正的父向选择器

.item:has(input:checked) { border-color: #0969da; }   /* 状态上移到容器 */
.card:has(img) { padding-top: 0; }                    /* 有封面图的卡片去掉顶部留白 */
li:has(> ul) { font-weight: 700; }                    /* 「有子列表」的项加粗 */
body:has(#search:focus) .hero { opacity: .35; }       /* 页面级联动,谨慎使用 */

参数里还能写相对关系::has(+ .next)(紧跟其后的兄弟存在)、:has(~ .x)(之后同级存在)、:has(> .x)(直接子元素)。它替掉了「用 JS 监听后给父加类」这一大类代码。

浏览器基线::has() 已在 Chrome / Edge / Safari / Firefox 全量支持(Firefox 121 起),属 Baseline 可用。代价是浏览器要在结构变化时重算,避免在超大列表上写高成本的选择器链(例如 body:has(...) 这种全局查询)。

:not() 的多参数与结构伪类

.btn:not(.primary, [disabled]) { … }    /* 逗号 = and,等价于串联两个 :not() */
li:nth-child(2 of .tag) { … }           /* 「.tag 里的第 2 个」 */
tr:nth-child(odd of :is(.tag, .note)) { … }
li:nth-last-child(-n+2) { … }           /* 倒数两项 */

:nth-child(An+B of S) 解决了「跳过表头后再数奇偶」「只在某类元素里数第 N 个」这两个长期只能靠 JS 的需求。浏览器基线:这一形态同样已是主流内核全量支持(Firefox 113 / Chrome 111 一线起)。

状态伪类:表单与交互的真相来源

选择器含义注意
:focus-visible只在「该显示焦点环」时命中(键盘导航)替代 outline: none 的正确写法,可访问性硬要求
:focus-within后代任一元素获得焦点时命中容器搜索框整块高亮的最省方案
:valid / :invalid校验状态未交互的空必填项也算 invalid,直接上红框会吓人
:user-invalid用户改过之后才判无效做红框提示比 :invalid 合适(较新内核)
:placeholder-shown占位符仍显示浮动 label 老配方,现在可用 :has() 更直观
:checked / :default / :indeterminate选中 / 默认选中 / 半选:indeterminate 只能由 JS 设置
:open<details> 展开纯 CSS 折叠面板的状态源(第十七章配合伪元素)
:targetURL 的 hash 指向它「定位高亮」零成本
:lang(zh)按 lang 匹配中英文的标点、字距、字体差异靠它

calc() 与混合单位

.el { width: calc(100% - 2 * 16px); }               /* %、px 必须在 calc 里才能相减 */
.el { font-size: calc(1rem + 0.5vw); }
.el { padding: max(12px, calc((100% - 640px) / 2)); }
.el { margin-top: calc(var(--n) * 6px); }

三条规则:+ - 两侧要有空格;* / 的乘除数必须是纯数字(带单位非法);参与长度计算的变量必须自带单位。clamp()/min()/max() 与 calc() 可以互相嵌套(第十三章的流式排版正是这个组合)。

更新的数学函数(round()、mod()、sign()、hypot()、tan() 等)已进入主流内核,用于生产前请用 @supports 检查(第二十章)。

什么时候不该用现代选择器

  1. 用 :has() 表达「需要 JS 才能算出的业务状态」——状态该由 JS 写类,选择器只负责样式;
  2. 在 :is() 里混入不支持的选择器,整条规则被丢弃;
  3. 用超长选择器链去「抢权重」——权重问题回第四章,靠更合理的类名结构解决;
  4. 依赖 @scope / :scope 做常规样式:仍在演进,生产项目请谨慎。

十六、原生嵌套与 @layer

CSS 规范补上了两件 Sass 早就有的能力:原生嵌套(少写重复上下文)与层叠层(把「谁优先」变成显式声明)。

示例 16:& 嵌套、条件规则内嵌、@layer 顺序表在新标签页打开

原生嵌套:& 是「外层选择器原文」

.card {
  border: 1px solid #d0d7de;
  &:hover { border-color: #0969da; }        /* → .card:hover */
  & .meta { color: #57606a; }               /* → .card .meta */
  & + .card { margin-top: 10px; }           /* → .card + .card */
  &.is-active { border-width: 2px; }        /* → .card.is-active —— 注意 & 与修饰符之间没有空格 */
  &:has(img) { padding-top: 0; }
  &::after { content: ""; }
}
.btn { &--primary { … } }                    /* → .btn--primary,BEM 修饰符最短写法 */

空格是有语义的:&--hot 是 .card--hot,& --hot 是后代选择器。这是嵌套最常见的笔误。

& 出现在最前面时可以省略( .card { h3 { … } }),但不建议——省略后隐含的是后代关系,权重和匹配范围都变了,可读性也下降。

条件规则可以写在规则内部,包括媒体查询、@supports、@container:

.menu { @media (max-width: 24rem) { & { font-size: 13px; } } }

浏览器基线:原生嵌套(含 &、条件规则内嵌)在 Safari 16.5 / Chrome 112 / Firefox 117 一线已全量支持,属 Baseline 可用。需要兼容更旧内核时,让构建工具(Sass、PostCSS、Lightning CSS)降级展开,不要在生产里裸写。

嵌套的代价:选择器膨胀

/* 四层嵌套写起来爽,展开后是这种怪物: */
.page .section .list .item:hover .title span { … }   /* 权重 (0,4,3),几乎无法覆盖 */

症状:想改一个标题颜色只能加 !important;改一处 DOM 结构样式全断。四条经验值:

  1. 嵌套深度 ≤ 2 层(组件本体 + 一个子元素或状态);
  2. 不要把布局上下文写进组件——组件不该知道自己被放在哪个 .page .section 里;
  3. 需要跨层级联动时用「变量或状态类」而不是选择器链(第十四章的组件变量);
  4. 权重问题回第四章解决,靠类名结构而不是嵌套堆叠。

@layer:把优先级写成一句声明

@layer reset, base, vendors, components, utilities, overrides;   /* 越靠后越优先 */

@layer base     { .panel { border-color: #bf8700; } }
@layer vendors  { .panel { border-color: #cf222e; } }   /* 后声明的层赢,与书写顺序无关 */

它解决的是老问题:第三方库和工具类的样式总在悄悄抢权重。有了层,你不再靠「写得靠后」或「加 !important」取胜,而是靠声明层顺序。

同名层会合并;@layer components { @layer button { … } } 形成 components.button 子层;还可以让第三方 CSS 进层而不改它源码:

@layer vendors { @import "reset.css"; }

完整的层叠顺序(必须记住的一张表)

从高到低:

  1. 进行中的过渡/动画声明(第十八章);
  2. 用户 !important(扩展、用户样式表);
  3. 作者 !important:未分层最高,各层之间则反向(越早声明的层越优先)——这条最反直觉;
  4. 未分层(unlayered)作者样式:比所有已声明的层都高;
  5. 分层作者样式:按 @layer a, b, c; 顺序,c > b > a,同层内回到第四章的权重规则;
  6. 用户样式表;
  7. 浏览器默认。

第 4 条是实践中最容易踩的:「随手加的一行补丁」默认就是最强,会压住你所有分层规则。想让补丁被层管理,就把它放进最末的层。

另外区分两个名字相近、机制无关的概念:层叠上下文(第十二章的几何问题)与层叠层级(本节的声明顺序问题)。

浏览器基线:@layer 与 layer() 函数在 Chrome 99 / Safari 15.4 / Firefox 103 一线已全量支持。用法建议:项目从零开始才引入分层;给已有大项目补层容易和未分层样式打架。

自查

  1. 「嵌套写了没生效」→ 检查 & 与空格(&--hot vs & --hot);
  2. 「层里的样式被随手一行盖掉」→ 那是未分层默认最高,符合规范,把补丁挪进 overrides 层;
  3. 「加了 !important 反而输给早层」→ 正确行为;
  4. 「DevTools 里看不懂谁赢」→ Styles 面板会显示层名与权重,逐条取消勾选验证(第二十章)。

十七、伪元素与生成内容

::before / ::after 是「不写进 HTML 的装饰盒子」。它的价值在于让表现性内容与文档结构分离——代价是这份内容对搜索、复制、辅助技术都不可靠。

示例 17:content 取值、counter 自动编号、::selection 与纯 CSS 装饰在新标签页打开

只有一个前提:必须有 content

.x::before { content: ""; }      /* 生成一个空盒子 —— 装饰全靠它 */
.x::before { content: none; }    /* 完全不生成,伪元素不存在 */

不写 content 时伪元素不会渲染。单冒号 :before 也能工作,但规范用双冒号区分伪元素与伪类(单冒号)。

content 的取值形态

.x::before { content: "文字"; }
.x::before { content: attr(data-icon) " "; }                    /* 属性值,可拼接 */
.x::after  { content: "(来源:" attr(data-src, "未标注") ")"; }  /* attr() 第二参数是回退值 */
.x::after  { content: url(arrow.svg); }                          /* 图片 */
.x::before { content: counter(n) "." counter(m); }               /* 计数器 */
.x::before { content: open-quote; }                              /* 引号,跟随 lang 与 quotes */
.x::before { content: "\201C 转义字符"; }                        /* Unicode 转义后要有空格 */

counter():章节自动编号

body { counter-reset: sect sub; }              /* 声明计数器(初值 0) */
.sect { counter-increment: sect; }
.sect::before { content: "第 " counter(sect) " 节:"; }
.sub { counter-increment: sub; }
.sub::before { content: counter(sect) "." counter(sub); }        /* 两级编号 */

counter() 的第二个参数可以指定样式:upper-roman、lower-alpha、cjk-decimal、decimal-leading-zero。它的价值不在替代 <ol>,而在「多级编号 + 非列表元素 + 跨元素拼字符串」这类真实需求(论文式标题、题号、章节引用)。

本站教程正文刻意不用自动编号:标题文字本身就是内容,编号写进正文而不是靠 CSS 生成,这样搜索结果、复制、读屏拿到的都是完整标题。

其它伪元素:一个比一个更「只能装饰」

伪元素能做什么限制
::before / ::after任意装饰盒子每个元素只有两个
::first-line / ::first-letter首行/首字样式(杂志风)只接受少量属性;::first-letter 不能作用于行内非替换元素
::marker列表记号的颜色、字重,甚至用 content 换成箭头只开放 content/color/font-* 等少数属性
::selection选中高亮配色只允许 color/background/text-shadow 等;对比度必须够
::placeholder占位符样式默认带透明度,需要显式覆盖
::backdrop<dialog>/全屏时遮罩层样式配合 color-mix() 做半透明遮罩很合适

五个纯 CSS 装饰配方

/* 圆形序号徽标:counter + inline-grid 居中 */
.steps { counter-reset: step; list-style: none; }
.steps li { counter-increment: step; }
.steps li::before { content: counter(step); display: inline-grid; place-items: center;
                    width: 20px; height: 20px; border-radius: 50%; background: #0969da; color: #fff; }
/* 尖角 tooltip:零尺寸盒子 + 四条透明 border,只给一侧上色 */
.tip::after { content: ""; position: absolute; top: 100%; border: 6px solid transparent; border-top-color: #24292f; }
/* hover 下划线:动 ::after 的 scaleX —— text-decoration 不能平滑过渡 */
.ul-anim::after { content: ""; position: absolute; left: 0; right: 0; bottom: -2px; height: 2px;
                  background: #0969da; transform: scaleX(0); transform-origin: left; transition: transform .22s; }
.ul-anim:hover::after { transform: scaleX(1); }
/* 骨架屏:background-position 动画,不需要图片 */
.skeleton { background: linear-gradient(90deg, #eaeef2 0 25%, #f6f8fa 37%, #eaeef2 63%) 0 0/400% 100%;
            animation: shimmer 1.4s ease infinite; }

(伪元素与过渡/动画的组合见第十八章;content 参与媒体查询做不同装饰也完全合法,见第十三章。)

三个必须知道的限制

  1. 不能嵌 HTML:content 里写 <b> 只会显示字面量。需要结构就写进 DOM。
  2. 不可选中、不可搜索、复制会丢失:所以「必需信息」不要只存在于伪元素里(价格、状态文字、必填星号的说明)。读屏支持参差不齐,把它当纯装饰最安全。
  3. 每个元素只有两个:需要三层装饰时——给父用 ::before、子元素用 ::after,或者改用真实 <span>、多重 box-shadow、多层 background(第八章)。

一条判断标准:图片的说明文字属于 HTML(alt),装饰符号属于 CSS(content)。前者是内容,后者是表现。

十八、过渡、变形与动画

动效的目标是「解释变化」,不是「装饰页面」。技术上只需要区分两件事:过渡负责两个状态之间,动画负责多帧序列。

示例 18a:transition 四要素、缓动曲线、transform 2D/3D 与性能友好属性在新标签页打开

transition 的四要素

.btn { transition: background-color 200ms ease-out 0s, border-color 200ms ease-in 50ms; }

顺序是 属性 / 时长 / 缓动 / 延迟;两个时间同时出现时第一个是时长、第二个是延迟,写反含义就变。

最常见的笔误是把 transition 只写在 :hover 里——那样只有悬停时有过渡,移开时瞬间跳回。正确做法是写在基础状态上:

.a { transition: all .3s; }        /* ✓ */
.a:hover { transform: translateY(-2px); }

不写 transition-property 时默认是 all,会带来意外的连锁动画(连布局属性都在补帧),建议显式列出要过渡的属性。

缓动决定手感

  • 进入用 ease-out(快起手、慢落位);
  • 离开用 ease-in;
  • 来回运动用 ease-in-out;
  • 需要「有重量」的弹跳用 cubic-bezier(.34, 1.56, .64, 1)(控制点 y 超出 1 会过冲回弹);
  • 精灵图/打字机用 steps(6, end);
  • 新写法 linear(0, .6 20%, 1 40%, .8 55%, 1) 直接按关键帧描述曲线,回弹不必再手算贝塞尔控制点。

一个常被忽略的细节:animation-timing-function 作用于每两个关键帧之间(不是整段动画),也可以写在关键帧内部逐段指定。

transform:2D、3D 与支点

.x:hover { transform: translate(6px, -4px) rotate(-6deg) scale(1.1); }
.wrap { perspective: 500px; }                 /* 给 3D 一个观察距离 */
.card:hover { transform: rotateY(180deg); }
.origin { transform-origin: top left; }       /* 旋转/缩放的支点 */

三条要点:

  1. 顺序有意义:transform 是矩阵相乘,先 translate 再 rotate 与反过来结果不同;
  2. translate 的百分比相对自身尺寸——这是「未知宽高也能居中」的依据(第十二章);
  3. scale 不改变布局占位,只是绘制时被放大,相邻元素不会让位(可能重叠)。

翻牌卡片:两面绝对定位叠在一起 + backface-visibility: hidden;立方体一类立体组合需要父级 transform-style: preserve-3d。

只动 transform 与 opacity

属性代价
transform、opacity只需合成,不触发布局与重排 → 动画首选
width/height/top/left/margin/padding触发布局(layout),多元素同时动会掉帧
box-shadow、border-radius、background-color触发重绘(paint),大面积时明显
用 left/top 做位移改用 transform: translate(),视觉一致、成本低得多

想给阴影加过渡时不要直接动 box-shadow(中间帧发灰且要重绘整块模糊),把阴影画在伪元素上、动它的 opacity:

.lift::after { content: ""; position: absolute; inset: 0; border-radius: inherit; z-index: -1;
               box-shadow: 0 8px 22px rgba(0,0,0,.22); opacity: 0; transition: opacity .22s; }
.lift:hover::after { opacity: 1; }
示例 18b:@keyframes、animation 属性族、stagger 与滚动驱动动画在新标签页打开

@keyframes 与 animation 属性族

@keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.item { animation: rise .5s ease-out both; animation-delay: calc(var(--i, 0) * 80ms); }

九个 longhand 里必须真正理解的三个:

  • animation-fill-mode:forwards 结束后停在最后一帧,backwards 让延迟期间就显示第一帧,进入动画通常写 both(否则元素会先闪一下原位再飞入);
  • animation-delay 可以为负值:表示从动画中段开始播,做错峰循环很方便;
  • animation-iteration-count 可以是小数:播到该比例停止。

多动画并列用逗号(animation: fade-in .6s both, nudge 1.4s infinite alternate;),第一个名字对应第一个时长。

进行中的动画声明优先级高于普通声明和 !important(第十六章的层叠表第 1 位)。所以「动画一跑 hover 就没反应」是规范行为,要先结束动画。

重放、暂停与错峰

/* 暂停:CSS 就能控制,不需要 JS 计时 */
.paused .box { animation-play-state: paused; }
/* 重新触发同名动画:必须先移除、强制回流、再加回(JS 里的一行技巧) */
el.classList.remove('run'); void el.offsetWidth; el.classList.add('run');
/* 错峰:把延迟交给变量,避免给每项写一条规则 */
.item { animation-delay: calc(var(--i) * 80ms); }   /* <li style="--i:2"> */

will-change:先测量再加

.box { will-change: transform; }   /* 提前把元素提升为合成层,避免动画开始时建层卡顿 */

三条纪律:只给「马上要动」的元素加(每个合成层都吃显存,几十层就负收益);动画结束后移除;现代内核已经会对 transform/opacity 动画自动分层,实测收益常为零——先用 DevTools 的 Rendering(Paint flashing)与 Layers 面板确认再决定。

副作用要记住:will-change 会创建层叠上下文与包含块,可能改变 fixed 的参考系(第十二章)。

滚动驱动动画(新增能力)

.bar { animation: grow linear both; animation-timeline: scroll(self); }
/* 也可以用命名滚动轴或元素可见度:animation-timeline: view(block 20% 60%) */

浏览器基线:滚动驱动动画目前在 Chromium 系(Chrome/Edge 115+)可用,Safari/Firefox 仍在演进,属渐进增强——必须保证「动画不跑时视图也成立」(例如进度条不跑时停在 0,而不是盖住内容)。落笔与上线前请以 caniuse 当前数据为准。

动效的三条纪律

  1. 只用于解释变化(位置、状态、加载进度),装饰性动效一律从简;时长 150–300ms 足够,超过 500ms 就会被感知为「慢」。
  2. 必须提供降级:@media (prefers-reduced-motion: reduce) 下关闭无限循环、把位移换成淡入(第十三章的全站写法)。
  3. 不要同时动很多个大面积元素:性能与注意力都是有限资源。

十九、视觉进阶:渐变、滤镜、遮罩与混合

这一章的工具全部服务于同一句话:让「不需要作为内容存在」的视觉,不用图片也能做出来。反过来也是判断标准——如果一块视觉承载了信息(截图、图表、产品图),它就该是 <img> 而不是 CSS。

示例 19:conic/radial 渐变、filter/backdrop-filter、mask 与 clip-path、blend在新标签页打开

渐变:从线性到锥形

四种函数构成全部渐变能力:

linear-gradient(135deg, #0969da, #8250df)                      /* 角度或关键词 */
radial-gradient(circle at 30% 30%, #fff 0 8%, #0969da 30%)     /* 形状 + 圆心 + 色标区间 */
conic-gradient(from 45deg at 50% 50%, red, gold, green, red)   /* 绕一圈,色相环/饼图 */
repeating-linear-gradient(...) / repeating-conic-gradient(...) /* 重复,条纹/放射纹 */

两个立刻能用的配方:

/* 硬切色带:同一位置写两次色标 → 进度条、双色块 */
.progress { background: linear-gradient(90deg, var(--brand-500) 62%, var(--bg-3) 62%); }

/* 环形进度:conic 上色 + mask 挖中心,纯 CSS 无 SVG */
.ring { aspect-ratio: 1; border-radius: 50%;
        background: conic-gradient(var(--brand-500) 0 var(--pct), var(--bg-3) var(--pct) 100%);
        mask: radial-gradient(circle, transparent 0 55%, #000 56%); }

多层渐变叠加是「有质感的封面」的标准做法:一层径向高光 + 一层细密纹理 + 一层底色渐变,配合 background-blend-mode 决定层间混合方式(本章示例最后那个 hero 就是三层)。

filter:改自己的绘制结果

filter: grayscale(1) saturate(1.6) blur(2px) drop-shadow(3px 4px 0 rgba(9,105,218,.5));
  • 可用函数:blur() brightness() contrast() grayscale() hue-rotate() invert() opacity() saturate() sepia() drop-shadow();
  • drop-shadow() 与 box-shadow 的区别:前者贴着透明轮廓画,适合异形/带透明通道的图片,后者只按矩形盒子;
  • 三个副作用:创建层叠上下文(第十二章)、改变 fixed 的参考系、动画 filter 成本高(模糊半径变化要重绘)。

实用配方:禁用态 grayscale(1) + opacity(.6);hover 轻微提饱和比加遮罩更省;缩略图统一色调用 filter 而不是重新处理图片。

backdrop-filter:毛玻璃

.glass { background: rgba(255,255,255,.18);
         backdrop-filter: blur(6px) saturate(1.4);
         -webkit-backdrop-filter: blur(6px) saturate(1.4);
         border: 1px solid rgba(255,255,255,.35); }

三条注意:需要 Safari 前缀;背后必须有内容才看得出效果(纯色底上等于白做);大面积高斯模糊在低端设备上很贵,别给整页背景加。那条半透明 border 是让玻璃「看得见边界」的关键,缺了它面板会糊成一片。

mask 与 clip-path:软边界与硬边界

maskclip-path
表达Alpha/亮度决定「哪里可见」,可以有渐变过渡几何裁剪,只有硬边界
语法与 background 几乎一致(mask-image/size/position/repeat)inset() circle() ellipse() polygon() path()
典型用途图片右缘渐隐、用 SVG 当图标形状(颜色交给 CSS)切角卡片、星形、hover 时从方到圆的形变
.fade-out { mask: linear-gradient(90deg, #000 40%, transparent); }        /* 列表末尾渐隐 */
.icon { mask: url(star.svg) center/contain no-repeat; background: currentColor; }  /* 随文字色变化的图标 */
.card { clip-path: polygon(0 0, 100% 0, 100% 70%, 70% 100%, 0 100%); }

两者都会创建层叠上下文,也都会裁掉溢出内容与阴影(第十二章的包含块规则同样适用)。clip-path 在两个形状函数相同、点数一致时可以平滑过渡——这是形变动效的做法;无障碍角度注意:被裁掉的文字仍会被读屏念到,视觉信息丢失要自己兜住。

mix-blend-mode 与 background-blend-mode

.overlay { mix-blend-mode: multiply; }        /* 与背后元素混合 */
.bg { background-blend-mode: multiply; }      /* 自己的多层背景之间混合 */

常用值:multiply(变暗,适合阴影/水印)、screen(变亮,适合光效)、overlay / soft-light(保对比的叠加)、difference / exclusion(反色文字)、hue / luminosity(给图片换色相)。

关键的一条:给混合区域加 isolation: isolate,否则它会跟整个祖先背景混合,页面别处会出现莫名其妙的颜色变化(同时你也顺手创建了层叠上下文,见第十二章)。

什么时候不要用这些

  1. 首屏大图:仍然应该 <img> + srcset(能缓存、能裁切、能懒加载、有 alt);
  2. 需要印刷/导出 PDF 的视觉:filter 与 mask 在打印管线里表现不一致;
  3. 大面积持续动画的 filter/backdrop-filter:低端设备掉帧明显(第十八章的性能原则)。

二十、容器查询、@supports 与调试排错

示例 20a:container-type、@container、cqi 单位与 :has() 组合在新标签页打开

两条语句就够

/* 一、把某个祖先声明为查询容器 */
.card-zone { container-type: inline-size; container-name: cardzone; }
/* 二、子树内部按它的尺寸写规则(查询的是祖先,样式给后代) */
@container cardzone (min-width: 340px) { .media { grid-template-columns: 128px 1fr; } }

也可以不写 container-name(匿名容器),但同名查询会命中「最近的」容器,跨组件复用时容易串味——给容器命名是更稳的习惯。简写形式:container: inline-size / cardzone;。

它的价值:同一张卡片放进侧栏(窄)、主区(中)、整页 banner(宽),组件源码完全相同,表现由容器决定。媒体查询回答「屏幕多大」,容器查询回答「我被放进多大的地方」——后者才是组件级响应式。

container-type 的三个值与代价

值可查询副作用
inline-size只有宽度轴最常用:不影响自身高度,不会产生循环依赖
size两个轴容器高度必须与内容无关(否则「内容决定高度、高度又决定内容」)——通常要显式给 height
normal不查询默认,取消容器身份

另外两条必须知道:container-type 非 normal 时,元素会建立独立格式化上下文(内部 margin 不再与外部塌陷,见第九章),并成为绝对定位后代的包含块(第十二章)。

容器单位:cqi / cqb / cqmin / cqmax

.title { font-size: 8cqi; }   /* 字号相对容器宽度的 1% */
.card  { padding: 2cqi; }
单位相对什么
cqi容器内联轴(水平书写模式下 = 宽度)的 1%
cqb容器块轴(= 高度)的 1%,需要 container-type: size 或 block
cqw / cqh宽度 / 高度的 1%
cqmin / cqmax两者中较小 / 较大者的 1%

与 vw 的关系一句话:vw 相对视口,cqi 相对最近的尺寸容器——组件内部一律用后者(第十三章的 clamp() 适合页面级排版,cqi 适合组件内)。

与媒体查询的分工

问题用什么
整页骨架变化(三栏 → 一栏、导航 → 汉堡)@media:这是页面级事实,只有视口能回答
组件在不同槽位里的形态@container
卡片墙要几列auto-fit + minmax()(第十一章),能不用查询就不用
字号随所在容器缩放cqi
系统偏好(深浅色、减少动效、指针类型)@media 特性查询(第十三章)

三者可以叠加:外层媒体查询定骨架,内层容器查询定组件形态。

与 :has() 组合,能替掉一大类 JS

@container (min-width: 420px) {
  .list { grid-auto-flow: column; grid-auto-columns: 1fr; }
  .list:has(> :nth-child(4)) { grid-auto-flow: row; grid-template-columns: repeat(2, 1fr); }
}

「容器够宽就横排,但项数 ≥4 时改成两列」——这条需求两年前必须写 JS,现在是两条 CSS 规则。

示例 20b:@supports 能力检测、DevTools 四面板与常见坑现场在新标签页打开

@supports:测试的是「声明」,不是「特性名」

@supports (display: grid) { … }
@supports (position: sticky) { … }
@supports (display: grid) and (not (display: subgrid)) { … }
@supports selector(:has(*)) { … }      /* 选择器支持测试:参数是选择器而不是声明 */

浏览器会去解析括号里的声明块,能接受就命中——所以判断准确率高,不需要维护特性名表。三个边界要知道:

  • 它只判断「语法是否被接受」,不判断实现是否有 bug 或只是部分支持;
  • 判断不了前缀差异(老 Safari 的 -webkit- 写法要并列写两份测试);
  • 不支持时整块规则被丢弃,因此默认(基线)样式必须自身成立。

三种写法模式:

/* A. 基线 + 增强(推荐) */
.bar { display: flex; gap: 8px; }
@supports (container-type: inline-size) { .zone { container-type: inline-size; } }

/* B. 新方案 + 显式降级 */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
@supports not (display: grid) { .grid > * { display: inline-block; width: 48%; } }

DevTools 四面板:各自回答什么问题

面板回答最有用的三个动作
Styles「哪条规则胜出、为什么」逐条取消勾选看变化;看被划掉的声明与旁边的警告图标;展开 :hover、层名(第十六章)与优先级数字
Computed「最终计算值是多少」看盒子图(margin/border/padding/content 各层实测像素,第六章);Ctrl+F 搜属性;展开 :root 看变量解析结果
Layout「几何问题出在哪」开启 sticky / scroll-overflow / Grid overlay,直接看轨道尺寸与被吸住的元素
Rendering「性能与状态」Paint flashing(绿闪=重绘)、Layers(合成层)、模拟 prefers-reduced-motion 与 prefers-color-scheme

三个不装插件就能做的动作:Elements 面板右键元素 → State… 强制 :hover/:focus/:checked;Styles 面板右上 ⋮ → Show all user-agent styles 看默认样式表;Network 面板禁用缓存 + 用无痕窗口排除扩展干扰。

六个高频坑的复现与修法

现象根因修法
height: 100% 不生效父级没有确定高度给父显式高度;或父用 Grid/Flex 靠 stretch;或整条链改用 min-height;或用 aspect-ratio/svh 表达「占满」
内容撑破容器Flex 子项 min-width 是 min-content(第十章)min-width: 0 + overflow-wrap: anywhere
意外的横向滚动条负 margin、width: 100vw(含滚动条宽度)、未换行长串、跑到外面的绝对定位Console 里过滤 el.scrollWidth > el.clientWidth 定位元凶;scrollbar-gutter: stable 防弹层抖动
margin 跑到父元素上塌陷穿透(第六章)父加 display: flow-root,或改用 gap
加了样式没变化选择器没命中,或被更高权重/更晚层覆盖(第四、十六章)Styles 面板看有没有列出这条、被划掉的是哪条
只在真机上不对视口单位(svh/dvh 与 vh)、安全区、hover/pointer 判定第十三章 + 第五章的相关条目

一行排查用的临时样式(只写在 DevTools 里,不要提交):

* { outline: 1px dashed rgba(255,0,0,.3); }   /* 谁比想象中宽/高,一眼看出 */
img { background: #ff0; }                      /* 看图片实际渲染尺寸 */
[style] { border: 2px solid red; }             /* 揪出内联样式 */

浏览器基线与降级策略汇总

本教程用到的较新能力,按「不支持会怎样」分类(版本号请以 caniuse 当前数据为准,这里给的是量级判断):

能力不支持时的表现要不要包 @supports
:has()、:is()/:where()、@layer、原生嵌套、gap、aspect-ratio、clamp()整条规则被忽略,布局退化为基线形态一般不需要,写清基线样式即可;追求极端兼容时包
容器查询与 cqi组件停在「窄容器」默认形态建议包,避免中间态视觉
subgrid卡片行不对齐,其他正常建议包
@property变量过渡退化成跳变不需要
滚动驱动动画进度条停在首/末帧必须包,且要保证静态形态可读
backdrop-filter半透明面板糊在一起建议包,并给一个不透明的回退底色

二十一、综合实战:把一团 div 变成现代布局

前面二十章的东西在真实项目里是同时发生的。这一章把一份「能跑但处处是隐患」的首页改写成现代写法,逐条对照。

改造前:八类典型问题

示例 21:改造前 —— 固定 960px、float 两列、颜色写死在新标签页打开

问题清单(每条都对应前面某一章):

  1. 固定像素宽度 + 居中(width: 960px; margin: 0 auto):窄屏出横向滚动条,宽屏两侧空白 → 第十三章 min(100% - 2rem, 68rem)。
  2. float 两列:需要空 div.clearfix、需要 .last 抹掉末位边距、需要 overflow: hidden 修高度塌陷 → 第十一章 Grid 命名区域 + gap。
  3. 卡片列数靠复制类名(两列一套、单列一套):改需求要两边同动 → 第十一章 auto-fit + minmax()。
  4. 颜色与字号硬编码且重复(#0066cc 出现 5 次、全部 px):暗色主题没有可能 → 第十四章令牌 + 第十三章 rem/clamp()。
  5. 靠堆权重覆盖(.container .content .main .item .t):任何定制只能 !important → 第四章降权重 + 第十五章 :is() + 第十四章组件变量。
  6. padding-top: 70px 手算垂直居中:字号一改就歪 → 第十章 align-items: center / 第十一章 place-items。
  7. 图片位靠 height 写死:真图进来要么变形要么留白 → 第十三章 aspect-ratio + object-fit。
  8. 只有 :hover 没有 :focus-visible:键盘用户看不见焦点,触屏点了不还原 → 第八/十五章焦点样式 + 第十三章 hover: hover 判定。

改造后:同一份内容,一半代码

示例 21:改造后 —— 令牌主题 + Grid/Flex + 容器查询 + 可降级动效在新标签页打开

改造后的写法逐条对应:

  1. 令牌层:--bg-1、--text-1、--brand-500、--space-3、--radius-3、--z-header 一次声明;html[data-theme='dark'] 只替换令牌,所有组件规则零改动,并配 color-scheme 让浏览器控件一起换(第十四章)。
  2. 容器层:.wrap { width: min(100% - 2 * var(--space-3), 68rem); margin-inline: auto; }——无需断点的居中容器(第十三章)。
  3. 骨架层:页头 position: sticky + z-index: var(--z-header);主体 Grid 单列,@media (min-width: 52rem) 变两列(minmax(0, 1fr) 防溢出,第十一/十二章)。
  4. 组件层:卡片一份样式,repeat(auto-fit, minmax(min(220px, 100%), 1fr)) 自己算列数;内部 flex-direction: column + margin-top: auto 让页脚对齐(第十/十一章)。
  5. 槽位自适应:侧栏列表声明 container-type: inline-size,@container (min-width: 22rem) 时才横排——同一个组件放进宽槽会自动变横排(第二十章)。
  6. 动效:hover 只动 transform 与伪元素 opacity,prefers-reduced-motion 下整组过渡被关闭(第十八章)。
  7. 可访问性::focus-visible 描边、aria-current="page" 标当前页、装饰色块加 aria-hidden、sr-only 补列表说明(第十五章 + HTML 教程第十七章)。
  8. API 化的组件:.badge { --badge-bg: …; --badge-fg: …; },使用方给祖先设变量就能定制,不需要修饰类(第十四章)。

对照检查清单(新项目起手就用它)

  • 是否有一条 * { box-sizing: border-box; }(第六章)
  • 颜色/间距/圆角/层级是否只出现在令牌里(第十四章)
  • 是否有硬编码 px 字号、是否给正文留了 rem 缩放余地(第七、十三章)
  • 布局是否用 Grid 定骨架、Flex 定一维,float 只留在图文环绕(第九~十一章)
  • 卡片墙是否用 auto-fit + minmax() 而不是复制列数类(第十一章)
  • 组件是否用容器查询而不是猜视口宽度(第二十章)
  • 每个可交互元素是否有 :focus-visible 样式,对比度是否达标(第八章)
  • 动效是否有 prefers-reduced-motion 降级(第十三、十八章)
  • 新特性是否包了 @supports,并确认不支持时的形态可接受(第二十章)
  • 图片是否有 width/height(防布局抖动)、aspect-ratio + object-fit 是否配合(第十三章 + HTML 教程第六章)
  • 是否存在靠 !important 或超长选择器维持的覆盖(第四、十五章)
  • 层叠顺序是否清晰:未分层样式有没有意外压过组件库(第十二、十六章)

结语:CSS 的心智模型只有三句话

  1. 谁生效 = 层叠(权重、书写顺序、!important、层、上下文)——第四、十二、十六章;
  2. 生效成什么样 = 盒模型 + 布局算法(普通流、Flex、Grid、定位)——第六、九~十二章;
  3. 能不能被所有人用 = 排版与可访问性 + 渐进增强(rem、焦点、降级、@supports)——第七、十三、二十章。

剩下的一切——颜色、渐变、阴影、动画——都是这三条之上的表现层。把这三句话记住,遇到「为什么不是我想的样子」时,先问它属于哪一句,答案通常就在对应那一章里。

评论

为保障内容质量,评论需站长审核通过后才会展示;提交后没立刻看到属正常,请勿重复提交。

还没有评论,来抢沙发~