2.3 伪类与伪元素


伪类与伪元素

本节摘要:伪类匹配元素的"状态"——被悬停、被聚焦、排第几、被选中;伪元素在元素内部创建"虚拟节点"——开头、结尾、首行、首字母。两者都不需要修改 HTML,就能实现交互反馈、斑马纹表格、杂志式首字下沉、纯 CSS 装饰图标等效果,是选择器体系里最见功力的一族。

学习目标

完成本节后,你将能够:区分伪类和伪元素的概念差异;使用动态伪类为交互状态添加视觉反馈;使用结构伪类按位置批量选择元素;使用 ::before::after 插入装饰内容和制作下划线动画;用纯伪类伪元素完成一个可交互的卡片效果。

伪类:元素的状态标签

伪类用一个冒号 : 开头,它匹配的是元素处于某种"状态"而非元素本身。同一个按钮,鼠标悬停时是一种状态,被按下时是另一种状态——CSS 用伪类给这些状态分别发了通行证。

动态伪类

动态伪类响应用户的交互行为——鼠标悬停、点击、聚焦等。这是 CSS 获得交互感的起点。

/* 链接的四种状态 */ a:link { color: #1976D2; } /* 未访问 */ a:visited { color: #7B1FA2; } /* 已访问 */ a:hover { color: #0D47A1; text-decoration: underline; } /* 鼠标悬停 */ a:active { color: #F44336; } /* 正在点击 */

这四条规则的顺序很重要,必须按照 LVHA(Link、Visited、Hover、Active)的顺序书写。如果 :hover 写在 :link 前面,:link 的样式会覆盖 :hover,导致悬停效果失效——因为四条规则特异性相同,胜负由源码顺序决定。业内有一个助记口诀:"LoVe HAte",爱与恨的顺序不能颠倒。

/* 表单输入框的聚焦状态 */ input:focus { outline: none; border-color: #1976D2; box-shadow: 0 0 0 3px rgba(25, 118, 210, 0.2); } /* 按钮的交互反馈 */ button:hover { background-color: #1565C0; transform: translateY(-1px); } button:active { transform: translateY(0); }

第二个例子是个经典小技巧:悬停时上移 1 像素制造"浮起来"的感觉,按下时归位制造"按下去了"的感觉。两个伪类、两行变换,按钮立刻有了生命感。

一个交互元素的状态流转

结构伪类

结构伪类根据元素在父容器中的位置来匹配,是"批量处理"的利器。

/* 第一个子元素 */ li:first-child { font-weight: bold; } /* 最后一个子元素去掉底边框 */ li:last-child { border-bottom: none; } /* 奇数行(斑马纹效果) */ tr:nth-child(odd) { background-color: #F5F5F5; } /* 偶数行 */ tr:nth-child(even) { background-color: white; } /* 第 3 个 li */ li:nth-child(3) { color: red; } /* 每 3 个中的第 1 个(1, 4, 7, 10...) */ li:nth-child(3n+1) { font-weight: bold; }

:nth-child() 是最强大的结构伪类。它接受一个公式 an+b,其中 n 从 0 开始递增取所有自然数。odd 等价于 2n+1even 等价于 2n3n+1 依次命中第 1、4、7、10 个。公式思维一旦建立,"每行三列的最后一列去掉右边距"这类需求就是一行代码的事。

表达式 匹配的元素 典型用途
:first-child 第 1 个 标题首项特殊样式
:last-child 最后 1 个 移除最后一项的底边框
:nth-child(odd) 奇数位 表格斑马纹
:nth-child(3n) 第 3、6、9... 每行 3 列的最后一列去右边距
:nth-child(n+4) 第 4 个起 隐藏超出数量的元素
:first-of-type 该类型的第一个 章节中第一个 h2 加粗

还有一个容易混淆的区分::nth-child() 看的是"在所有兄弟中的位置",:nth-of-type() 看的是"在同类型兄弟中的位置"。如果列表里混着不同类型的元素,两者的结果会不同。

<div class="demo"> <h3>小标题</h3> <p>段落一</p> <p>段落二</p> </div>
/* p:nth-child(2) —— 选中"段落一":它是所有孩子中的第 2 个 */ /* p:nth-of-type(2) —— 选中"段落二":它是 p 家族中的第 2 个 */

小标题占据了大排行第 1 位,但不参与 p 的内部计数——两套坐标系,各数各的。

否定伪类

:not() 接受一个选择器作为参数,排除匹配该选择器的元素。做"除它之外都要"的样式时不用再写覆盖规则。

/* 所有 input 除了提交按钮 */ input:not([type="submit"]) { border: 1px solid #ccc; padding: 8px; } /* 所有段落除了带 special 类的 */ p:not(.special) { font-size: 16px; }

UI 状态伪类

表单控件还有一组状态伪类,跟控件的实时状态联动,做表单校验提示时非常好用:

/* 复选框被勾选时,让旁边的文字变粗 —— 需要配合兄弟选择器 */ input:checked + label { font-weight: bold; color: #1565C0; } /* 占位符显示时输入框半透明提示 */ input:placeholder-shown { border-color: #ccc; } /* 填写合法后即时变绿,不需要任何 JavaScript */ input:valid { border-color: #4CAF50; } input:invalid { border-color: #D32F2F; }

最后一组值得停留几秒:邮箱格式填对了边框变绿、填错了变红——:valid:invalid 配合 HTML 的类型校验,零脚本实现了即时反馈。

伪元素:虚拟的内容节点

伪元素用两个冒号 :: 开头(CSS3 规范;CSS2 时代也是一个冒号,所以老代码里的 :before 依然有效),它们创建了一个虚拟的元素节点,让你可以为元素的特定部位添加样式或插入内容。

::before::after

这两个伪元素在元素内容的开头和结尾各插入一个虚拟节点。它们必须配合 content 属性使用——没有 content,伪元素不会出现。

/* 在每个引用块前面加大引号装饰 */ blockquote::before { content: "\201C"; /* 左双引号的 Unicode 编码 */ font-size: 48px; color: #1976D2; line-height: 1; } /* 在新窗口打开的链接后面加小箭头 */ a[target="_blank"]::after { content: " ↗"; font-size: 12px; color: #999; } /* 用伪元素做装饰性前缀 */ .tag::before { content: "#"; color: #1976D2; font-weight: bold; }
<span class="tag">CSS</span> <span class="tag">前端</span> <!-- 渲染效果:#CSS #前端 -->

注意第一个例子和上一节的联动:blockquote::before 配合 Unicode 编码插入引号,a[target="_blank"]::after 配合属性选择器给外链加标识——选择器家族之间是可以自由组合的。

::after 还有一个流传最广的工程用法——清除浮动,第 4 章会专门讲它的原理:

.clearfix::after { content: ""; display: table; clear: both; }

::first-line::first-letter

排版向的两个伪元素,让普通段落瞬间有杂志质感:

/* 段落首行加粗 */ p::first-line { font-weight: bold; } /* 段落首字下沉(杂志风格) */ .drop-cap::first-letter { font-size: 3em; float: left; line-height: 1; margin-right: 8px; color: #1565C0; }

::first-line 的特殊之处在于它不是一个"真实"的盒子——浏览器渲染时才知道首行到哪结束,窗口变窄时首行会重新计算,样式始终跟着"当前的第一行"走。这种"延迟决定"的特性是伪元素与普通元素最本质的区别。

伪类 vs 伪元素

对比维度 伪类 :hover 伪元素 ::before
符号 单冒号 : 双冒号 ::(CSS3)
本质 元素的特殊状态 虚拟的内容节点
是否新增内容 不能 可插入装饰内容
典型用途 交互反馈、位置选择 装饰内容、清除浮动
必须配合 content 属性

一句话分辨:问"它是一种状态吗"——是就是伪类;问"它是凭空多出来的一块吗"——是就是伪元素

动手做:纯 CSS 交互卡片

把本节知识拧成一股绳,做一个不需要任何 JavaScript 的交互卡片——悬停时上浮、出现渐变下划线动画、角标旋转。HTML 只有一个元素:

<article class="post-card"> <h3>CSS 结构伪类笔记</h3> <p>nth-child 公式 an 加 b 的用法整理……</p> </article>
.post-card { position: relative; /* 为角标定位做参考,第4章详讲 */ max-width: 320px; padding: 20px; border: 1px solid #E0E0E0; border-radius: 8px; background: #fff; transition: transform 0.2s, box-shadow 0.2s; } /* 角标:伪元素插入内容,绝对定位到右上角 */ .post-card::before { content: "NEW"; position: absolute; top: 12px; right: 12px; background: #FFB300; color: #fff; font-size: 12px; padding: 2px 8px; border-radius: 4px; transition: transform 0.3s; } /* 标题下划线:伪元素从 0 宽度展开成 100% */ .post-card h3 { position: relative; display: inline-block; } .post-card h3::after { content: ""; position: absolute; left: 0; bottom: -4px; width: 0; height: 2px; background: #1565C0; transition: width 0.3s; } /* 悬停状态:卡片上浮、角标旋转、下划线展开 */ .post-card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); } .post-card:hover::before { transform: rotate(8deg) scale(1.1); } .post-card h3:hover::after { width: 100%; } /* 斑马纹内容:nth-child 给段落交替底色 */ .post-card p:nth-of-type(odd) { background: #FAFAFA; }

保存刷新,把鼠标移上卡片:整体上浮并投下阴影,右上角角标轻微旋转放大,标题下面一条蓝线从左向右展开。三个效果分别用了 :hover 与伪元素的组合、伪元素做"零 HTML 的装饰节点"、结构伪类做位置样式——而且全程没有一行 JavaScript。这个卡片可以直接并入你的个人主页作品列表。

💡 动手建议:给个人主页的导航链接也做下划线动画——把 ::after 设为一条细线,默认宽度 0,悬停时过渡到 100%,和上面卡片的标题做法完全一样。再把代码里的 0.3s 改成 1s0.1s 各试一次,感受过渡时长对"手感"的影响。

⚠️ content 属性的值可以是字符串、attr() 函数(读取元素属性值)、计数器或空字符串 ""。如果忘记写 content,伪元素不会显示——这是最常见的"伪元素不生效"原因。另外伪元素插的是"装饰",不是"内容"——正文信息不要塞进 content,屏幕阅读器读不到它。

常见疑问解答

问::hover 在手机上怎么表现? 触屏设备没有悬停概念,触摸点击会模拟一次"悬停加按下",之后这个元素可能一直保持悬停态,直到点到别处。所以移动端别把关键信息只放在悬停效果里——菜单如果是悬停展开的,触屏用户可能永远打不开。现代方案是用媒体查询区分指针类型,或干脆用点击交互替代悬停交互。

问:伪元素插入的内容能被选中复制吗? 通常不能,这既是特性也是限制。content 里的文字对屏幕阅读器大多不可见、用户选中复制也常常失败——所以正文信息绝不能塞进伪元素,它只承载装饰。反过来,你想防止别人复制的版权声明小尾巴,用伪元素倒是歪打正着。

问::nth-child:eq 这种下标选择怎么区分? CSS 原生只有"第几个"的序号语义(从一开始数),没有 jQuery 那种零开始的 :eq。另外注意 :nth-child(2) 严格等于"第二个孩子",没有范围含义;想要"第二个起全部"要写 :nth-child(n+2)——公式思维又一次立功。

问:一个元素同时命中 :hover:focus,样式会叠加吗? 会。不同伪类是不同的"状态维度",各自的声明并行生效;改了同一个属性才比较优先级,而 :hover:focus 特异性相同,胜负由书写顺序决定。给交互元素写状态样式时记得把焦点态也照顾到——键盘用户靠 Tab 导航,焦点态是他们唯一的路标。

交互反馈的设计分寸

伪类给了页面"活的"能力,但用得狠了也会翻车——什么都动,等于什么都不突出。几个经过检验的分寸感:动效只加在"有动作含义"的元素上,链接和按钮是天然候选,纯展示的文字就别悬停变色了;状态变化要克制,变色、位移一两像素、阴影微增,三选一即可,全部叠上是游乐场;过渡时长控制在两百到三百毫秒,快于一百毫秒感觉不到、慢过四百毫秒显得迟钝。

可访问性视角还有一条容易被忽略:动效会引发部分前庭疾病用户的不适,大面积的位移和缩放尤其明显。稳妥的做法是只对小元素做微小的位移动画,并在用户系统开启"减少动态效果"偏好时收敛动画——现代 CSS 提供了专门响应该偏好的媒体特性,进阶动画主题里会展开。

状态样式还有个"完整性"问题:写了 hover 别忘了 focus。键盘用户用 Tab 在页面上导航,焦点就是他们的光标——如果只有悬停有反馈,键盘用户会"迷路"。默认的焦点轮廓被 outline: none 去掉时,务必补一套自定义的 focus 样式(就像本节开头那个带光晕的输入框),否则等于拆了路标。

最后把本节的选择器按"写在哪最合适"归拢一下:交互反馈交给动态伪类,位置规律交给结构伪类,排除项交给否定伪类,装饰与内容插入交给伪元素。四类各管一段,互不越权——当你发现自己在 HTML 里反复加"第一个""最后一个""奇数行"之类的类名时,就该意识到那是结构伪类的场子;当你在 HTML 里手写引号、箭头、井号这类装饰字符时,伪元素正等着接管。选择器写得好不好,最终体现在 HTML 是否保持了它本来的干净模样。

本节要点回顾

  • 伪类捕获状态:动态伪类(LVHA 顺序不能乱)、结构伪类(nth-childan+b 公式)、否定伪类(排除法)、UI 状态伪类(:checked:valid 零脚本做校验反馈)。
  • 两套计数:nth-child 按大排行,:nth-of-type 按同类型排行,混排结构里结果不同。
  • 伪元素造节点::before/::after 必须配 content::first-line/::first-letter 做杂志排版。
  • 组合威力:伪类配属性选择器(外链加箭头)、伪类配伪元素(悬停展开下划线)、伪元素配清除浮动。
  • 装饰不承载内容content 里只放装饰性文字,真实内容永远写在 HTML 里。

选择器家族到此集结完毕。但当它们同时瞄准一个元素、还都想改同一个属性时,浏览器听谁的?下一节的重头戏——优先级与层叠——就是这场争端的终审法庭。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U