Appearance
伪类、伪元素与内容生成
元信息
- 目标:握住"设计时/运行时"这把尺子——作者设计时写定的是静态的类、元素、内容,浏览器运行时算出或造出的是动态三样:伪类(动态类,按运行状态即时挂上、自动重估)、伪元素(动态元素,渲染树里的盒子)、内容生成(动态内容);:has() 的动态类要看后代;以 clearfix 为合体案例体会"样式问题在样式层解决"
- 关键概念:设计时、运行时、伪类、伪元素、内容生成、渲染树、:hover、:has()
- 关联阶段:cp1
- 常见误区:以为 :hover/:visited 是设计师在 HTML 里定义的类(伪类是浏览器运行时挂的动态类);以为选择器只能选中标记元素(伪元素是渲染时才出现的动态元素);以为 ::before 生成的内容能用 querySelector 选中(它只在渲染树、不在 DOM 树);以为 CSS 永远选不中"父元素"(:has() 已经可以)
动机:设计时与运行时
CSS基础的选择器前三类(标记名、命名、结构关系),查的都是设计时写定在文档里的静态之物。本篇讲选择器如何指称另一半世界:运行时才知道的事实。
作者写页面的那一刻是设计时,落进 HTML 的全是静态的:类是写死的 class="card",元素是写好的标记,内容是手打的文字。页面跑起来的每一刻是运行时,大量事实设计时根本不知道:鼠标此刻在哪个元素上、这条链接访问过没有、窗口此刻多宽、表单里哪一项非法。CSS 是声明式语言,作者只描述"想要什么",那就必须能指称这些运行时的事实——总不能为了"鼠标在上面时变黑"去写事件监听脚本(那是第 7 周的路,也违背样式与行为分离)。办法是给三样静态物各配一个动态的对应物,由浏览器在运行时替作者算出或造出:
| 静态:设计时,作者写定 | 动态:运行时,浏览器算出/造出 | |
|---|---|---|
| 类 | class="card" | 伪类::hover、:checked、:has(...) |
| 元素 | HTML 里的标记 | 伪元素:::first-line、::before 的盒子 |
| 内容 | 手打的正文 | 内容生成:content 产出的文字与图形 |
「动态」的准绳是设计时定不下来、运行时才算得出,不是"时刻在变"——:first-child 平时纹丝不动,可一插入兄弟元素它立刻换人,仍是运行时的事实。静态三样住在 DOM 树里,设计时就在;动态三样不在文档里,运行时才出现——所以 querySelector 永远够不着 ::before,后文展开。
伪类:运行时的动态类
静态类是作者设计时写进 HTML 的;伪类是浏览器运行时按状态即时挂上的类。链接的种种状态,就好像浏览器内部替它挂上了 class="visited" 之类的标记——类是"伪"的,效果是真的。
机制上,这套"挂类"随时可以重估:浏览器替你跟踪交互状态,状态一变,受影响的元素重新过一遍"收集命中、级联裁决"(见CSS基础),样式自动跟上。你写 :hover 时并不知道会选中谁,浏览器运行到"鼠标正在上面"那一刻才替你选中——不写一行 JS 事件监听,状态跟踪是浏览器送的。它与媒体查询的断点重估同族:声明条件,浏览器负责盯着,条件翻转样式自动重算。
动态类不只盯交互状态::focus(获得输入焦点)、:checked(勾选态)、:first-child(第一个子元素,结构一变它也重算)都是。
:has():看后代的动态类
普通动态类只看元素自己的状态,:has() 的类还要看它的后代——元素"里面有什么"也成了挂类的依据,俗称"父选择器",CSS 等了二十多年的能力:
css
form:has(input:invalid) { border: 2px solid red; } /* 表单里有非法输入时,给表单本身上样式 */
.card:has(img) { padding: 0; } /* 含图片的卡片不留内边距 */为什么等了二十多年?普通匹配从最右端开始、只朝祖先方向问,便宜就便宜在"单问"(见CSS基础);:has() 要回答"我里面有没有匹配的后代",必须等后代全部定局,这个动态类才算得出来,匹配方向整个被反转——主流引擎为此重构了样式匹配模块,才敢让它落地。
一个实用玩法:纯 CSS 的主题开关——复选框勾选与否,让 html:has(#switch:checked) 里的变量翻面(变量见现代CSS语法)。第 7 周之前我们还不写 JS,靠它就能做交互。
伪元素:运行时的动态元素
静态元素是作者写进 HTML 的标记;伪元素是渲染时才出现的元素,文档里没有对应的节点。两种来历:
- 从内容里析出:
::first-letter、::first-line——字符和行本就存在,但没人标记过它们,渲染时按需划出盒子。第一行划到哪儿随窗口宽度变,拖一拖下面的示例,这个动态元素的边界就在你眼前重算 - 凭空生成:
::before、::after——样式层造出来的盒子,下一节的内容生成就是给它填东西的机制
注意现代写法用双冒号 ::first-letter,与伪类的单冒号区分。首字下沉(drop cap,段落首字放大占几行)正是它的一手经典应用——印刷时代传下来的排版手法,如今一行选择器就成。主线一的排印与主线二的选择器,在这里握了一次手。
内容生成:运行时的动态内容
正文该由作者手打,可有一类内容天生属于样式层:引号装饰、列表编号、小图标——它们跟着结构走、不承载信息,写进 HTML 就是污染结构(爬虫与读屏软件都要陪它绕路)。"样式与结构分离"落到这里,就是让样式层在运行时自己产出内容:content 属性给命中的动态元素挂上"凭空造出"的文字或图形。比如给引文配装饰性引号——页面上看得到,源代码里没有:
机制上,::before/::after 造出的是只在渲染树、不在 DOM 树的盒子:querySelector 选不中它,文档源码里也没有它——不是你写错了,它压根不是文档的一部分,是渲染时才造出来的呈现物(两棵树的分工见关键渲染路径)。也正因为此,重要内容别放这里(读屏软件对生成内容的支持参差不齐,搜索引擎也未必收录)——装饰合适,样式的归样式。
日常用途还有给链接补图标(见字体的图标字体)、给段落编号(配合计数器)——凡"内容跟着结构走、而非作者手写"的场合。
合体案例:clearfix
浮动的旧病是高度塌陷:子元素全部漂走后父元素没了高度依据。早期补救是在 HTML 末尾补一个挂 clear 的空标记,能用,但为样式污染了结构。clearfix 用本篇的动态武器,让样式层自己造这个"清除点":
css
.clearfix::before,
.clearfix::after {
content: " ";
display: table;
}
.clearfix::after { clear: both; }读法:伪元素凭空造出节点之外的盒子(::after),内容生成负责往里填东西(content),合起来在父元素末尾放上一个背着 clear 的空盒,HTML 零改动。至于这个盒子为什么一挂 clear 就能把父高度撑回来——机制是 clear 的规范定义,归第 6 周;学到那边回看这里,正是本篇的动态武器在渲染树里替布局干活。
小结
- 一把尺子:设计时/运行时。静态的类、元素、内容由作者写定在 DOM 里;动态三样——伪类(动态类)、伪元素(动态元素)、内容生成(动态内容)由浏览器运行时算出或造出
- 伪类 = 运行时按状态即时挂上的类,
:hover/:visited/:checked都属此类;状态一变样式自动重估,零 JS :has()的动态类要看后代,必须等后代定局,所以等了二十多年- 伪元素(:: 双冒号)分两种来历:从内容析出(first-letter/first-line,随窗口宽度重算)与凭空生成(before/after)
- 生成的内容只在渲染树、不在 DOM 树——querySelector 够不着是特性,不是故障
- clearfix 是动态三件套的合体案例:样式层造"清除点",撑高度的机制归第 6 周
延伸
- 浮动、高度塌陷与 clear 的机制见前现代布局
- 渲染树与 DOM 树的分工见关键渲染路径
- 用变量 +
:has()做主题切换,见现代CSS语法 - 完整的伪类/伪元素清单 https://developer.mozilla.org/zh-CN/docs/Web/CSS/Reference#选择器