在这里插入图片描述

我收到你的大纲了!这题目太真实了,谁刚学前端没被这俩货坑过啊。行,我这就开整,保证给

好,我收到你的大纲了!这题目太真实了,谁刚学前端没被这俩货坑过啊。行,我这就开整,保证给你整出一篇像刚喝完奶茶在办公室吐槽写出来的东西,代码管够,废话也管够(但不是那种AI废话)。


伪类和伪元素傻傻分不清?前端新人避坑指南(附实战用法)

刚入门前端那会儿,我真以为 ::before 和 :hover 是一个妈生的

说实话,我现在看到当年自己写的代码都想穿越回去抽自己。记得第一次面试的时候,面试官问我:“你说说伪类和伪元素有啥区别?” 我当时那个自信啊,张口就来:“这不都是一个冒号两个冒号的区别嘛,功能都差不多,都是选不到的东西用来选。”

面试官那个表情我至今难忘,就是那种"这孩子废了"的微笑。

后来进了公司,我亲眼看到组里一个大佬用 ::before 做了个超酷的按钮 hover 效果,我兴冲冲地抄过来,把双冒号改成单冒号 :before,心想这不都一样嘛还少打个冒号呢。结果?毛反应都没有。我当时还在那调试了半小时,怀疑人生到以为浏览器坏了。最后大佬路过,瞥了一眼我的屏幕:“你这不是伪元素吗?单冒号那是 IE8 的古董写法,而且你 content 呢?没 content 你写一百个 before 也白搭啊兄弟。”

那一刻我才意识到,这俩玩意儿根本不是一家人,连远房亲戚都算不上。

HTML里那些看不见摸不着却能改天换地的小妖精

前端这个活儿最魔幻的地方就在这儿——你写的 HTML 明明就那么多标签,但到了页面上,总感觉有些"东西"存在,可你 F12 打开开发者工具翻个底朝天也找不到它们的具体标签。就像你感觉房间里有人,但开灯一看啥也没有,这种灵异事件在 CSS 界天天上演。

比如你给按钮写了个 :hover,鼠标一放上去颜色变了,可 HTML 里哪有这个"hover 状态"的标签啊?再比如你写了个 ::before,在里面塞了个小图标,DOM 树里你也找不到这个图标的 div 或者 span,但它确实就在那里,还占位置,还能点(如果你设置的话)。

这些小妖精就是我们要聊的主角。搞清楚它们的底细,你写 CSS 的时候就能从"蒙眼射箭"进化到"指哪打哪",而且从调试到性能优化都能玩转。别觉得这是基础中的基础就不当回事,我见过三年经验的前端还在用 JS 实现那些伪元素三行代码就能搞定的事儿,组长看了直摇头。

别再把 :first-child 和 ::first-letter 混着用了,面试官都笑出声了

来,先做个小测试,看你现在能不能分清。下面这几个选择器,哪些是伪类,哪些是伪元素:

:first-child
::first-letter
:nth-of-type(2)
::placeholder
:active
::selection

如果你脑子里还需要反应一下,那这篇文章你看对了。其实有个特别简单的判断方法,我后面会详细说,但现在先记住:伪类是关于状态的,伪元素是关于内容的

:first-child 是在说"这个元素是家里老大"这件事,是一种状态;而 ::first-letter 是在说"我要把这个段落的第一个字母单独拎出来打扮一下",是在操作内容。一个是"你是谁",一个是"我要造个新东西"。

面试的时候如果连这都要想半天,真的,面试官内心已经在给你打分了——不是什么高分。因为这意味着你写 CSS 的时候思路是乱的,不知道自己在操作什么,出了 bug 也只能靠试。

从"我以为"到"原来如此":伪类 vs 伪元素血泪辨析

好了,不吓唬你了,咱们正经(相对正经)开始掰扯这两兄弟。

伪类其实是给元素加状态标签

你想想,CSS 选择器最基本的是啥?是标签名、class、id,对吧?这些都是实实在在的, HTML 里写了个 div class="box",那你写 .box 就能选中它。但问题是,有些事情是动态发生的,比如你鼠标移上去了,或者这个 input 被禁用了,或者这是列表里的第一个li——这些"身份"或者"状态"是临时性的,你不可能在 HTML 里给每个状态都写个 class 吧?

<!-- 你不会想这么干的 -->
<div class="box hover">鼠标在这呢</div>
<div class="box normal">鼠标走了</div>

这得多累啊,而且鼠标一移走你还得用 JS 去改 class。伪类就是来解决这个的。它就像是给元素贴了个临时标签,告诉浏览器:“我要选中那个正在被鼠标hover的家伙”、“我要那个被禁用掉的 input”。

:hover、:focus 这些不是样式,是元素的"情绪开关"

咱们用最最常见的 :hover 开刀。很多人以为 :hover 是个样式,其实不是,它是选择器的一部分,是用来选中元素的。只不过选中的时机很特殊——只有当鼠标指针悬停在元素上的时候,这个选择器才生效。

/* 这个选择器的意思是:选中所有 class 为 btn 且正在被鼠标悬停的 button 元素 */
button.btn:hover {
    background-color: #ff6b6b;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
    transition: all 0.3s ease;
}

/* :focus 同理,选中的是当前获得键盘焦点的 input */
input.form-control:focus {
    border-color: #4ecdc4;
    outline: none;  /* 去掉那个丑丑的默认蓝框 */
    box-shadow: 0 0 0 3px rgba(78, 205, 196, 0.2);
}

你看到了吗?:hover:focus 前面都有个冒号,后面跟着的是常规的 CSS 属性。它们就是个筛选条件,筛的是"此刻处于某种状态的元素"。

:nth-child(2n) 这种选择器背后藏着数学鬼才的执念

这个选择器我当年看了半天文档才搞懂,其实特别实用,特别是做表格或者列表的时候。nth-child 里面的参数可以是个数字,可以是关键词(odd/even),也可以是公式。

/* 基础用法:选中第3个li */
li:nth-child(3) {
    background: gold;
}

/* 关键词:奇数行和偶数行,做斑马纹表格必备 */
tr:nth-child(odd) {
    background-color: #f8f9fa;
}
tr:nth-child(even) {
    background-color: #ffffff;
}

/* _formula 模式:这才是精髓 */
/* 2n 表示 2×n,n 从0开始计数,所以选中的是第0、2、4、6...个,也就是偶数 */
li:nth-child(2n) {
    margin-right: 0;  /* 每行两个 item 时,去除偶数项的右边距 */
}

/* 3n+1 表示第1、4、7、10...个,也就是每3个中的第一个 */
.grid-item:nth-child(3n+1) {
    clear: both;  /* 清除浮动,开始新的一行 */
}

/* 负数也能用,选中前5个 */
li:nth-child(-n+5) {
    font-weight: bold;
}

/* 从第6个开始往后全选 */
li:nth-child(n+6) {
    opacity: 0.6;
}

这里有个坑我得提醒你:nth-child 数的是所有兄弟元素,不限于同类型。比如你有 ul 里面混杂着 li 和 div,li:nth-child(2) 选中的可能不是第二个 li,而是第二个子元素(如果第一个是 div),前提是那个 div 也得是第二个子元素。如果你只想在同类元素里数,得用 :nth-of-type

表单验证神器 :valid/:invalid,用户填错都不用 JS 提醒

这玩意儿配合 HTML5 的表单验证属性,能让你少写好多 JS。input 有个 pattern 属性或者 type="email" 这种,浏览器会自动帮你验证格式对不对,然后用 :valid:invalid 就能抓到状态。

<form class="signup-form">
    <div class="form-group">
        <input 
            type="email" 
            class="email-input" 
            placeholder="输入你的邮箱"
            required
        >
        <span class="error-msg">邮箱格式不对啊朋友</span>
    </div>
    
    <div class="form-group">
        <input 
            type="password" 
            class="pwd-input" 
            pattern=".{8,}" 
            placeholder="至少8位密码"
            required
        >
        <span class="success-msg">密码长度够了</span>
    </div>
</form>
/* 默认隐藏提示信息 */
.error-msg, .success-msg {
    display: none;
    font-size: 12px;
    margin-top: 5px;
}

/* 当 input 值无效时,显示错误信息 */
.email-input:invalid + .error-msg {
    display: block;
    color: #e74c3c;
}

/* 当密码有效时,显示成功信息 */
.pwd-input:valid + .success-msg {
    display: block;
    color: #27ae60;
}

/* 还可以给边框染色 */
input:invalid {
    border-color: #e74c3c;
    background-color: rgba(231, 76, 60, 0.05);
}

input:valid {
    border-color: #27ae60;
}

看到那个 + 号了吗?这是相邻兄弟选择器,意思是"紧接在 input 后面的 .error-msg"。这样连 JS 都不用写,用户一边输入,浏览器一边验证,CSS 一边改样式,体验丝滑得很。而且这完全是纯 CSS 实现的,性能比你写一堆 oninput 事件监听强多了。

伪元素根本就不是真实 DOM 节点

好了,现在进入玄幻区域。伪元素这玩意儿,真的是"无中生有"。你先记住一句话:伪元素是 CSS 凭空造出来的 DOM 节点,HTML 里根本不存在

::before 和 ::after 是 CSS 凭空造出来的"幽灵孩子"

每个 HTML 元素(大部分,后面会说例外),不管你写没写,CSS 都当你有两个隐藏的插槽:一个叫 ::before,一个叫 ::after::before 是在元素内容的最前面插入,::after 是在最后面插入。

<!-- 你写的 -->
<div class="box">我是内容</div>

<!-- CSS 伪元素造出来的结构大概相当于(只是比喻,真实DOM里并没有) -->
<div class="box">
    <::before>伪元素内容在这里</::before>
    我是内容
    <::after>伪元素内容在这里</::after>
</div>

这两个"幽灵孩子"默认是行内元素(inline),但你可以给它们设置各种样式,什么宽高、背景、边框、定位,统统可以。这就给了 CSS 极大的施展空间。

.box {
    position: relative;
    padding: 20px;
    background: #ecf0f1;
    border-radius: 8px;
}

/* 在 box 内容前面加个引号装饰 */
.box::before {
    content: '"';
    font-size: 40px;
    color: #bdc3c7;
    position: absolute;
    top: -10px;
    left: 10px;
    font-family: Georgia, serif;
}

/* 在 box 内容后面加个"查看更多"的箭头 */
.box::after {
    content: '→';
    margin-left: 8px;
    color: #3498db;
    transition: transform 0.3s;
}

/* hover 时箭头往右动一动 */
.box:hover::after {
    transform: translateX(4px);
}
content 属性才是伪元素的灵魂,没它你写一百个 ::after 也白搭

这里我要用血的经验教训提醒你:如果你写了 ::before 或者 ::after 却忘了写 content 属性,那这个伪元素根本不会出现

/* 错的 */
.box::before {
    width: 10px;
    height: 10px;
    background: red;
    /* 没有 content,啥也不显示 */
}

/* 对的,哪怕 content 为空字符串也行 */
.box::before {
    content: '';  /* 空内容也可以,但至少得有 */
    width: 10px;
    height: 10px;
    background: red;
    display: inline-block;  /* 行内元素设宽高要转一下display */
}

content 不只是能写字符串,还能干很多骚操作:

/* 用 attr() 读取 HTML 属性的值 */
a::after {
    content: ' (' attr(href) ')';  /* 在链接后面显示实际的 URL */
    color: #95a5a6;
    font-size: 0.8em;
}

/* 用 counter() 实现自动编号 */
ol.custom-list {
    counter-reset: item;  /* 重置计数器 */
}
ol.custom-list li::before {
    content: counter(item) '. ';  /* 使用计数器 */
    counter-increment: item;  /* 计数器递增 */
    color: #e74c3c;
    font-weight: bold;
}

/* 甚至可以放 Unicode 字符 */
.warning::before {
    content: '\26A0';  /* 警告符号 ⚠ */
    color: #f39c12;
    margin-right: 8px;
}
清除浮动老古董 clearfix 为啥非得靠 ::after?真相在这

还记得浮动布局时代的噩梦吗?父元素里的子元素都 float 了,父元素就塌了,高度变成0。以前人们用各种方法,什么额外加个空 div 啊,overflow:hidden 啊,但最优雅的还是 clearfix,核心就是 ::after

/* 经典的 clearfix */
.clearfix::after {
    content: '';  /* 必须要有,哪怕是空的 */
    display: table;  /* 或者 block,table 是为了兼容一些老版本IE */
    clear: both;  /* 清除左右浮动 */
}

/* 现在用 flow-root 更现代,但要兼容老浏览器还是得 clearfix */
.modern-clearfix {
    display: flow-root;
}

原理很简单:::after 在父元素内容最后插入了一个看不见的块级元素,这个元素设置了 clear: both,它必须排在所有浮动元素后面,所以就把父元素的高度撑开了。这招妙就妙在不需要改动 HTML 结构,纯 CSS 解决,而且 ::after 不会出现在 DOM 里,不影响 JS 操作。

它们长得像双胞胎,但户口本差十万八千里

现在咱们来做个彻底的大对比,把这俩货的底细扒清楚。

伪类操作的是"已有元素的某种状态",伪元素是"凭空插入新内容"

这是本质区别。伪类像是一个筛选器,从已有的元素里挑出一些符合特定条件的;伪元素是一个构造函数,凭空造出新的内容盒子。

/* 伪类:从所有 a 标签里,挑出那些被访问过的 */
a:visited {
    color: purple;
}

/* 伪元素:在 p 标签里造出一个只包含第一个字母的盒子 */
p::first-letter {
    font-size: 3em;
    float: left;
    margin-right: 8px;
    color: #2c3e50;
}

语法上冒号数量不是玄学:一个冒号是伪类,两个是伪元素(CSS3 规范)

这条是硬性规定。CSS3 为了区分这两者,规定伪元素都用双冒号 ::,伪类用单冒号 :

/* 伪类家族(单冒号) */
:hover
:focus
:active
:first-child
:nth-child(3)
:not(.exclude)
:is(h1, h2, h3)  /* 新的 :is 也是伪类 */

/* 伪元素家族(双冒号) */
::before
::after
::first-line  /* 第一行文字 */
::first-letter  /* 第一个字母 */
::selection   /* 用户选中的文字 */
::placeholder /* input 的占位符文字 */

但是,历史上有些伪元素(before 和 after)在 CSS2 里是用单冒号的,所以为了兼容老的浏览器和代码,现代浏览器通常也接受 :before 这种写法,不会报错。但你自己写的时候,务必用双冒号,这是为了可读性和规范性,而且一些新的伪元素(比如 ::selection)是不支持单冒号的。

浏览器开发者工具里看结构:伪类高亮原元素,伪元素单独列一行

打开 F12,这是最能直观感受到区别的方法。当你选中一个元素,如果是伪类在生效,你看到的是那个元素本身样式变了,元素结构没变。但如果是伪元素,你会在 Elements 面板里看到 ::before 或者 ::after 单独列在元素下面,就像它真的有个子元素一样。

而且伪元素在开发者工具里是可以单独勾选显示/隐藏样式的,就像操作真实 DOM 一样,但你右键检查它,是定位不到具体 HTML 源码的,因为它根本就不在源码里。

实际开发中那些骚操作全靠它们撑场面

理论知识说了一堆,来点实际的,看看这俩玩意儿在真实项目里能玩出什么花。

按钮 hover 加个小箭头?那是 ::after 在偷偷打工

这种设计很常见:按钮默认状态有个文字,鼠标一放上去,右边冒出个小箭头,或者小图标。

<button class="fancy-btn">
    立即购买
</button>
.fancy-btn {
    position: relative;  /* 给伪元素定位当参照 */
    padding: 12px 40px 12px 24px;  /* 右边留出空间给箭头 */
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 25px;
    font-size: 16px;
    cursor: pointer;
    transition: all 0.3s ease;
}

/* 箭头用 ::after 做 */
.fancy-btn::after {
    content: '→';
    position: absolute;
    right: 20px;
    top: 50%;
    transform: translateY(-50%);
    opacity: 0;  /* 默认隐藏 */
    transition: all 0.3s ease;
}

/* hover 时按钮变宽,箭头出现并往右移 */
.fancy-btn:hover {
    padding-right: 48px;  /* 给箭头让出更多空间 */
    box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}

.fancy-btn:hover::after {
    opacity: 1;
    right: 24px;  /* 稍微移动一下,有动效 */
}

你会发现我没用 JS,甚至没在 HTML 里加那个箭头,全部 CSS 搞定。而且这段代码复用性极强,只要是 class 为 fancy-btn 的按钮,自动就有这个效果。

列表项前加彩色圆点不用 img?伪元素 content + background 搞定

UI 设计师有时候会给列表项前面设计各种花式 bullet,彩色的、方形的、带阴影的,甚至可能是小图标。你别傻傻地切图或者用 img 标签,伪元素三行代码搞定。

<ul class="feature-list">
    <li>极速响应</li>
    <li>安全可靠</li>
    <li>24小时服务</li>
</ul>
.feature-list {
    list-style: none;  /* 干掉默认的黑点 */
    padding-left: 0;
}

.feature-list li {
    position: relative;
    padding-left: 24px;  /* 给伪元素留位置 */
    margin-bottom: 12px;
    line-height: 1.6;
}

/* 用 ::before 做彩色圆点 */
.feature-list li::before {
    content: '';  /* 空内容,我们用背景色 */
    position: absolute;
    left: 0;
    top: 0.5em;  /* 垂直居中技巧 */
    width: 8px;
    height: 8px;
    background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
    border-radius: 50%;  /* 圆形 */
    box-shadow: 0 2px 4px rgba(245, 87, 108, 0.3);
}

/* 甚至能给不同行的圆点不同颜色 */
.feature-list li:nth-child(1)::before {
    background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
.feature-list li:nth-child(2)::before {
    background: linear-gradient(135deg, #30cfd0 0%, #330867 100%);
}
.feature-list li:nth-child(3)::before {
    background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}

看到 :nth-child::before 联手了吗?伪类负责选第几个 li,伪元素负责画圆点,完美配合。

表单输入框获得焦点时边框变色?:focus 一招鲜吃遍天

这个前面提过了,但值得再深化一下。现在流行那种"浮动标签"(Float Label)效果,就是输入框里原本有 placeholder,一点击输入,那个 placeholder 变小跑到上面去当 label。这可以用 :focus 配合 ::placeholder 和一些技巧实现,但更简单的是用 :focus 配合 sibling 选择器。

<div class="input-group">
    <input type="text" id="username" placeholder=" ">
    <label for="username">用户名</label>
    <div class="border-line"></div>
</div>
.input-group {
    position: relative;
    margin-bottom: 20px;
}

.input-group input {
    width: 100%;
    padding: 10px 0;
    font-size: 16px;
    border: none;  /* 干掉默认边框 */
    border-bottom: 2px solid #ddd;  /* 用底边框当线 */
    outline: none;
    background: transparent;
}

.input-group label {
    position: absolute;
    top: 10px;
    left: 0;
    font-size: 16px;
    color: #999;
    pointer-events: none;  /* 让鼠标事件穿透 label,不然点不到 input */
    transition: 0.3s ease all;
}

/* 底边框变色用伪元素做(当然直接给 input 加 border-color 也可以,但伪元素可以做动画效果) */
.input-group .border-line {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 0;
    height: 2px;
    background: #3498db;
    transition: width 0.4s ease;
}

/* 核心:当 input 获得焦点时,label 上浮变小,底边框变长 */
.input-group input:focus ~ .border-line,
.input-group input:not(:placeholder-shown) ~ .border-line {
    width: 100%;
}

.input-group input:focus ~ label,
.input-group input:not(:placeholder-shown) ~ label {
    top: -20px;
    font-size: 12px;
    color: #3498db;
}

这里 :focus 选中了获得焦点的 input,然后用 ~(通用兄弟选择器)选中后面的 .border-linelabel,改变它们的样式。placeholder-shown 也是个伪类,表示当 placeholder 正在显示时(也就是 input 值为空时)。

奇偶行背景交替?:nth-child(even) 让表格瞬间高级感拉满

这个前面也提过,但实战代码可以更完善一点,特别是一些细节处理。

<table class="data-table">
    <thead>
        <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>部门</th>
            <th>薪资</th>
        </tr>
    </thead>
    <tbody>
        <tr><td>001</td><td>张三</td><td>技术部</td><td>15k</td></tr>
        <tr><td>002</td><td>李四</td><td>设计部</td><td>12k</td></tr>
        <tr><td>003</td><td>王五</td><td>技术部</td><td>18k</td></tr>
        <tr><td>004</td><td>赵六</td><td>市场部</td><td>10k</td></tr>
        <tr><td>005</td><td>钱七</td><td>技术部</td><td>20k</td></tr>
    </tbody>
</table>
.data-table {
    width: 100%;
    border-collapse: collapse;  /* 合并边框,避免双边框 */
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.data-table th,
.data-table td {
    padding: 12px 16px;
    text-align: left;
    border-bottom: 1px solid #e0e0e0;
}

/* 表头样式 */
.data-table thead th {
    background-color: #f8f9fa;
    font-weight: 600;
    color: #495057;
    text-transform: uppercase;
    font-size: 0.85em;
    letter-spacing: 0.5px;
}

/* 斑马纹:偶数行背景色不同 */
.data-table tbody tr:nth-child(even) {
    background-color: #f8f9fa;
}

/* hover 效果:当前行高亮 */
.data-table tbody tr:hover {
    background-color: #e9ecef;
    cursor: default;
    transition: background-color 0.2s;
}

/* 第一列加粗 */
.data-table td:first-child {
    font-weight: 600;
    color: #212529;
}

/* 最后一列(薪资)右对齐,并且绿色显示 */
.data-table td:last-child {
    text-align: right;
    color: #28a745;
    font-weight: 500;
}

/* 可以结合伪元素给薪资前面加个 ¥ 符号 */
.data-table td:last-child::before {
    content: '¥';
    margin-right: 2px;
    color: #6c757d;
}

这一段代码里,nth-child(even):hover:first-child:last-child 全用上了,表格瞬间就像个正经的企业级应用了,而且代码量极小。

踩过的坑比写的代码还多

好了,爽完了该泼冷水了。这俩玩意儿坑特别多,我踩过的坑能填满一个西湖。

写了 ::before 却不设 content,结果啥也不显示——别问,问就是默认值为空

这个前面强调过了,但我还得再说一遍,因为真的太容易忘了。特别是当你想用伪元素纯粹当做一个装饰性的色块或者形状时,你脑子里想的是"我要一个红色的方块",手一滑就写了宽高背景色,然后页面上一片空白,你怀疑 CSS 文件没加载,怀疑选择器写错了,怀疑人生,最后发现就是少了个 content: ''

而且 content 的默认值确实是 none,不是空字符串。如果你写 content: none,在某些老浏览器里可能还有问题,所以记住:永远写 content: ''

想用伪元素做绝对定位却忘了父元素 position:relative,元素直接飞出银河系

这是 CSS 新手必犯的错,不只是伪元素,所有绝对定位都这样。但伪元素特别容易中招,因为你觉得"我就加个小装饰",随手写个 position: absolute,结果它跑到页面左上角去了,或者干脆不见了。

/* 错的 */
.box {
    width: 200px;
    height: 200px;
    background: #eee;
}
.box::after {
    content: 'badge';
    position: absolute;
    top: -10px;
    right: -10px;
    /* 结果这个 badge 跑到 body 的右上角去了,因为 .box 没设 relative */
}

/* 对的 */
.box {
    position: relative;  /* 建立定位上下文 */
    width: 200px;
    height: 200px;
    background: #eee;
}
.box::after {
    content: 'badge';
    position: absolute;
    top: -10px;
    right: -10px;
    background: red;
    color: white;
    padding: 4px 8px;
    border-radius: 4px;
}

永远记住:绝对定位元素会相对于最近的非 static 定位的祖先元素定位。如果你没给父元素设 relative/absolute/fixed/sticky,它就一路往上找,直到找到 body。

在 img、input 这些自闭合标签上用 ::before?浏览器:我不认!

这个坑隐蔽性极强。有些元素是置换元素(Replaced Element),比如 imginputtextareaselectiframe。这些元素在渲染的时候,内容会被外部资源替换掉(比如图片文件、表单控件),它们本身没有"内容区域"的概念,所以伪元素(before/after)在它们身上是无效的。

/* 这些都不会生效 */
img::before {
    content: '加载中...';  /* 没用 */
}

input::after {
    content: '✓';  /* 没用 */
}

br::before {
    content: '我无敌了';  /* 更没用 */
}

如果你确实需要在 input 或者 img 前面加东西,包一层 div

<div class="input-wrapper">
    <input type="text">
</div>
.input-wrapper {
    position: relative;
}
.input-wrapper::before {
    content: '🔍';
    position: absolute;
    left: 10px;
    top: 50%;
    transform: translateY(-50%);
}
.input-wrapper input {
    padding-left: 35px;  /* 给图标留位置 */
}

伪类顺序乱写导致 :visited 样式失效?LVHA 原则早该刻进 DNA

如果你给链接写样式,涉及到 :link:visited:hover:active,顺序写错了,有些效果会出不来。记住口诀 LVHA

  1. Link(未访问链接)
  2. Visited(已访问链接)
  3. Hover(鼠标悬停)
  4. Active(鼠标按下)
/* 正确顺序 */
a:link { color: blue; }
a:visited { color: purple; }
a:hover { color: red; }
a:active { color: green; }

/* 如果写成这样,hover 和 active 可能就失效了 */
a:hover { color: red; }
a:link { color: blue; }  /* 后面覆盖前面,hover 被覆盖了 */

原理是 CSS 的层叠和优先级。:hover 必须放在 :link:visited 后面,:active 必须放在 :hover 后面。虽然现在 :link 用得少了(一般都直接写 a 标签选择器),但如果你严格区分未访问和已访问状态,这个顺序还是得注意。

让代码又快又稳的土味技巧

最后分享一些实战中攒下来的小经验,土,但是好用。

能用伪类实现交互就别急着上 JS,性能省一半

鼠标悬停显示下拉菜单?:hover 搞定。输入验证实时反馈?:valid/:invalid 搞定。当前页面导航高亮?:current(或者给 body 加 class 配合 .nav-item.active)搞定。

JS 操作 DOM 是有成本的,特别是事件监听、修改 class、重排重绘。能用 CSS 解决的,浏览器原生帮你优化好了,丝滑得很。

纯元素做装饰性图标,比切图快还省 HTTP 请求

简单的几何图形(三角形、箭头、分割线、小圆点),用伪元素 + CSS 画出来,不需要请求图片资源,渲染快,还能随意改颜色、大小,响应式也友好。

比如画个简单的三角形:

.arrow-down {
    position: relative;
}
.arrow-down::after {
    content: '';
    position: absolute;
    top: 50%;
    right: 10px;
    width: 0;
    height: 0;
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 8px solid #333;
    transform: translateY(-50%);
}

比用 img src=“arrow.png” 香多了。

调试时给伪元素加个 border,不然你永远不知道它跑哪去了

伪元素看不见摸不着,出问题的时候特别难调。我的习惯是,写的时候先给它加个明显的边框或背景色:

.debug::before {
    content: '';
    border: 1px solid red;  /* 调试时加上 */
    /* ... 其他样式 */
}

确认位置对了再删掉。或者直接在开发者工具里勾选 ::before 的样式,给它加个 outline: 2px solid red !important;,立竿见影。

兼容性兜底:IE8 只认单冒号,但谁还在乎 IE8 啊(狗头)

如前面所说,如果你真的很不幸要兼容 IE8( condolences),记得 :before:after 用单冒号。如果是现代浏览器,坚持用双冒号,这是语义正确的做法,而且一些新的伪元素(::selection::placeholder::backdrop 等)是不支持单冒号的,养成好习惯,省得以后改。

当你终于分清它们那天,组长说:现在流行用 Web Components 了……

就像你现在看到的这篇文章,从入门到入土,从 :hover::part(Web Components 里的伪元素,用来穿透 Shadow DOM styling),CSS 的世界一直在变。

但万变不离其宗,理解伪类是状态、伪元素是内容这个核心区别,你再去学什么新的选择器都能举一反三。哪怕以后真用上了 Tailwind 或者 CSS-in-JS,这些底层的概念依然是你的根基。

好了,扯了这么多,代码也贴了这么多,希望你看完这篇不像技术文章的技术文章,能真的把这俩"妖精"给驯服了。下次面试再被问到,别像我一样支支吾吾,直接甩出这段:“伪类是单冒号,操作已有元素的状态;伪元素是双冒号,凭空插入新内容。content 是伪元素的命根子,relative 是绝对定位的锚点。”

妥妥的。

至于组长说的 Web Components……害,先让我把这周的需求交了再说吧,谁爱学谁学去。

(全文完,代码拿去随便 copy,出了 bug 别找我,找就是浏览器兼容性 issues)

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁) 学习路线(点击解锁) 知识定位
《微信小程序相关博客》 持续更新中~ 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》 持续更新中~ AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》 Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》 持续更新中~ 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》 持续更新中~ Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》 持续更新中~ SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》 持续更新中~ 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》 持续更新中~ 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》 持续更新中~ 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》 持续更新中~ 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》 持续更新中~ 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐