伪类与伪元素深度解析:before/after 实用案例
一、先分清两个东西
很多人把伪类和伪元素混为一谈,其实它们是 CSS 的两套完全不同的系统。
| 伪类(Pseudo-class) | 伪元素(Pseudo-element) | |
|---|---|---|
| 选择器写法 | 一个冒号 :hover |
两个冒号 ::before |
| 本质 | 描述元素的状态 | 创建一个不存在的虚拟元素 |
| 能否加样式 | ✅ 能 | ✅ 能(因为它真的生成了节点) |
| DOM 中是否存在 | 否,只是选择器匹配规则 | 否,但渲染树中真实存在 |
⚠️ 旧标准写一个冒号(
:before)也能用,现代写法用两个冒号(::before)以区分伪类。实际项目中两者都兼容,但建议统一用双冒号。
一句话区分:伪类是"它现在什么状态",伪元素是"凭空造出一块东西"。
二、伪类:状态选择器
高频伪类速查
| 伪类 | 含义 | 典型场景 |
|---|---|---|
:hover |
鼠标悬停 | 按钮变色 |
:active |
按下瞬间 | 点击反馈 |
:focus |
获得焦点 | 输入框高亮 |
:focus-within |
自身或子元素获得焦点 | 表单容器高亮 |
:first-child |
第一个子元素 | 去掉首项 margin |
:last-child |
最后一个子元素 | 去掉末项 padding |
:nth-child(n) |
第 n 个子元素 | 斑马条纹 |
:not(.xxx) |
排除某类 | 排除禁用状态 |
:is(.a, .b) |
匹配任一 | 简化重复选择器 |
:has(.xxx) |
包含某子元素 | 父级联动(CSS4) |
:has() --- 伪类里的杀手级特性
css
css
/* 父元素包含 checked 的 input 时,改变自身样式 */
.card:has(input:checked) {
border-color: #4f46e5;
background: #eef2ff;
}
以前这事只能用 JS 干,现在纯 CSS 一行搞定。
三、伪元素:凭空造节点
::before / ::after 核心规则
css
css
.element::before {
content: ""; /* ❗ 必须有,否则不渲染 */
display: block; /* 或 inline-block,决定它的盒子模型 */
width: 100px;
height: 100px;
/* ...其他样式 */
}
三个铁律:
content必须写,空字符串""也行,不写等于没这个元素- 默认
display: inline,不占宽高,必须手动改block或inline-block - 它是元素的第一个/最后一个子节点,不是兄弟节点
四、before/after 实用案例(10 个)
案例 1:清除浮动(经典中的经典)
css
css
.clearfix::after {
content: "";
display: table;
clear: both;
}
现在有了 Flex/Grid,这个用法在减少,但维护旧项目时天天见。
案例 2:自定义列表序号
css
css
li::before {
content: "✓ ";
color: #22c55e;
font-weight: bold;
margin-right: 8px;
}
效果:不用 <span> 标签,纯 CSS 造出绿色对勾。
案例 3:装饰性引号
css
css
blockquote::before {
content: "「";
font-size: 48px;
color: #94a3b8;
line-height: 0;
vertical-align: -0.5em;
margin-right: 4px;
}
blockquote::after {
content: "」";
font-size: 48px;
color: #94a3b8;
line-height: 0;
vertical-align: -0.5em;
margin-left: 4px;
}
效果:引用文字自动被大号引号包裹,HTML 里一个字符都不用加。
案例 4:输入框聚焦动画(下划线展开)
css
css
.input-wrapper {
position: relative;
}
.input-wrapper::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background: #6366f1;
transition: all 0.3s;
transform: translateX(-50%);
}
.input-wrapper:focus-within::after {
width: 100%;
}
效果:输入框聚焦时,底部出现一条从中间展开的彩色下划线。
案例 5:图片叠加渐变遮罩
css
css
.card-img::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0,0,0,0.7), transparent);
}
效果:图片底部渐变变暗,文字直接叠加在图上清晰可读,不用加任何 <div> 遮罩层。
案例 6:气泡对话框小三角
css
css
.tooltip::after {
content: "";
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #1e293b;
}
效果:纯 CSS 画出一个向上的小三角,气泡框指向下方元素。
案例 7:徽章/角标(未读消息红点)
css
css
.badge::after {
content: "3";
position: absolute;
top: -6px;
right: -6px;
width: 18px;
height: 18px;
background: #ef4444;
color: white;
font-size: 11px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
效果:右上角红色圆形角标,数字居中,零 JS 零图片。
案例 8:加载动画(纯 CSS spinner)
css
css
.spinner::after {
content: "";
display: block;
width: 20px;
height: 20px;
border: 3px solid #e2e8f0;
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
效果:元素后面自动出现旋转加载圈,比 <img> 轻量一百倍。
案例 9:分栏装饰线(伪元素做分隔符)
css
css
.price::before {
content: "¥";
font-size: 14px;
vertical-align: super;
color: #94a3b8;
}
.old-price::after {
content: attr(data-original); /* 读取 HTML 属性值 */
text-decoration: line-through;
color: #94a3b8;
font-size: 12px;
margin-left: 8px;
}
HTML:
ini
html
<span class="price" data-original="199">99</span>
效果:¥99 ~~199~~,原价从 data 属性读取,不用写死在 HTML 里。
案例 10:多行文本省略 + "展开"按钮
css
css
.text-clamp {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.text-clamp::after {
content: " 展开";
color: #6366f1;
cursor: pointer;
font-weight: 500;
}
效果:文本超过 3 行自动省略,末尾自动跟一个"展开"链接,纯 CSS 实现。
五、content 能放什么?
| 值 | 说明 |
|---|---|
"" |
空字符串(最常用,造盒子用) |
"文字" |
直接显示文本 |
attr(href) |
读取元素属性值 |
counter(num) |
读取计数器值 |
url(...) |
插入背景图(注意是背景,不是 img 标签) |
none |
不生成伪元素 |
六、避坑清单
| 坑 | 正确做法 |
|---|---|
::before 没写 content |
必写 content: "",否则整个伪元素不存在 |
伪元素用了 float |
伪元素默认 inline,浮动会导致 display 变为 block,行为不可控 |
| 想给伪元素加点击事件 | ❌ 不可能,伪元素不在 DOM 树中,无法绑定事件 |
::before 和 ::after 嵌套 |
❌ 不支持,伪元素不能再生成伪元素 |
| 屏幕阅读器读不到伪元素内容 | ✅ content 里的文字会被读到,但装饰性内容建议用 "" |
七、一张图总结
ruby
伪类 : 选择"状态" → :hover :focus :nth-child :has()
↓
不创造节点,只改变匹配规则
伪元素 :: 创造"节点" → ::before ::after ::placeholder ::selection
↓
真的在渲染树中生成了一个虚拟盒子
能加宽高、背景、动画、内容
但不在 DOM 中,JS 抓不到
核心结论:伪类让你精准选中"某种状态的元素",伪元素让你在不改 HTML 的前提下凭空造出一块可样式化的区域。before/after 是 CSS 里最被低估的瑞士军刀------它不是装饰,是真正的布局工具。