☰
现代CSS布局基石:Flex与Grid双剑合璧,伪元素选择器实战指南
2026/10/16 6:53:48 网站建设 项目流程

2. 现代 CSS 布局的核心基石:Flex 与 Grid 双剑合璧

说到现代 CSS 布局,最常见的两个“扛把子”就是 Flexbox 和 Grid。很多人对它们的关系有误解,总觉得有了 Grid 就不需要 Flex,或者 Flex 更简单就先学 Flex。实际上这两者解决的问题维度完全不同,配合好了才是真正的效率翻倍。

2.1 Flex 是“一维流”,适合对齐与分布

Flexbox 的设计目标是解决“在一个方向上如何排列元素”,它的核心是“主轴”和“交叉轴”两个概念。主轴方向通过flex-direction控制,默认是row(横向),也可以变成column(纵向)。这决定了你在布局时是从左往右排,还是从上往下排。

我见过不少新人把 Flex 当成“万能布局工具”,做什么都用它,结果遇到复杂的二维网格就写得非常痛苦。Flex 真正擅长的是:导航栏里的间距分配、按钮组里的自适应宽度、卡片内部的对齐、多行文本的截断等。比如下面这段代码:

.nav { display: flex; align-items: center; justify-content: space-between; gap: 16px; }

如果你要做垂直居中加上水平均分,这个写法几乎是最优解。注意gap这个属性,以前在 Flex 布局里是没有的,必须用 margin 加上各种 hack,现在所有主流浏览器都支持gap了,别再写得像 2016 年一样了。

再强调一个细节:Flex 的交叉轴对齐使用align-items,主轴对齐使用justify-content,它俩经常被搞混。我的记忆技巧是:“j”开头的是字母表第 10 个,排在“i”前面,所以justify-content是主轴;“align”开头是“a”,排在“j”前面,所以交叉轴。这虽然是个土办法,但真的帮我在面试时避免过低级错误。

2.2 Grid 是“二维棋盘”,适合整体页面骨架

Grid 布局则是把容器划分成一个个格子,你可以精确控制行和列。它在做整体页面骨架、复杂二维区域布局时非常顺手。比如后台管理系统的侧边栏 + 顶栏 + 内容区三明治结构,用 Grid 是最清晰的:

.layout { display: grid; grid-template-columns: 240px 1fr; grid-template-rows: 64px 1fr; grid-template-areas: "sidebar header" "sidebar content"; height: 100vh; }

这段代码里,我用了grid-template-areas这种“命名区域”的方式。相比纯粹用grid-row/grid-column数字定位,命名的好处是阅读代码的人一眼就能看出哪个模块在哪,协作时沟通成本低很多。我司现在做中后台项目,页面骨架几乎全部采用这种写法,新同学上手极快。

有人会问:那我什么时候用 Flex,什么时候用 Grid?我的经验是:如果容器里的子元素数量会动态变化,或者你只需要在一个方向上进行排列,优先 Flex;如果页面结构固定,且横纵方向都有对齐需求,优先 Grid。还有人说“Grid 能实现 Flex 的所有功能”,理论上部分可以,但写起来明显更啰嗦,别为了炫技牺牲开发效率。

2.3 二者混合使用的经典场景

实际业务中,最常用的组合是:外层用 Grid 定骨架,内层每个模块再用 Flex 做细节对齐。举一个常见的商品卡片例子:

.card { display: grid; grid-template-rows: auto 1fr auto; } .card-title { display: flex; justify-content: space-between; align-items: baseline; }

这样整个卡片的“骨架”稳定,内部标题和价格又能在水平方向自由对齐。我在复盘自己写过的代码时发现,凡是二者混合使用得当的地方,后期维护都相对轻松;反而那些从头到尾只用一种布局方式的项目,改起来想摔键盘。

3. 伪元素选择器入门:它到底是什么

CSS 选择器系列写到了第 18 期,前两期我们聊了基础选择器和伪类选择器,这期终于轮到伪元素选择器——就是::before、::after、::first-line这些。它俩虽然名字只差一个字,但底层逻辑完全不同。伪类是描述“元素的某个状态”的,比如:hover表示鼠标悬停时,元素本身还是那个元素,只是样式变了。而伪元素是“凭空造出一个不存在的节点”,你可以给它设置内容、加背景、做动画,它就像一个真实 DOM 元素一样参与渲染。

举个例子你就明白了:

.box::before { content: "前缀"; color: red; }

上面这行代码执行后,浏览器会在.box内部的最前面插入一个“假的文本节点”,文本内容是“前缀”,颜色是红色。这个节点你没法在 DevTools 的 Elements 面板里像普通节点那样直接修改文本,只能通过 CSS 控制。这就是“伪元素”的“伪”字含义——它不是真实存在的 DOM,却真实参与了渲染。

伪元素能解决的问题非常多:给列表加装饰圆点、给卡片加角标、给输入框加清空按钮、给引用块加引号、清除浮动……这些需求如果都靠往 HTML 里塞真实元素,代码会又脏又乱。伪元素的最大价值就是让你“不收钱地往页面上加东西”,既不影响 HTML 结构,又能满足视觉需求。

注意:伪元素选择器的语法是双冒号::,伪类是单冒号:。这是 CSS3 时代定下的规范,唯一例外是::before/::after为了兼容 IE8,可以写成单冒号:before/:after。其他伪元素一律要求双冒号,否则在现代浏览器里可能直接失效。

3.1 常用伪元素速查表

我们先把常见的伪元素拉个清单,再看看每个怎么用:

伪元素作用常用场景
::before在元素内容前插入伪节点装饰图标、前缀文本、角标
::after在元素内容后插入伪节点清除浮动、后缀文本、气泡箭头
::first-line匹配块级元素的第一行文本首行加粗、首行变色
::first-letter匹配块级元素的第一个字符首字下沉、首字母大写
::selection匹配用户选中的文本自定义选中背景色
::placeholder匹配输入框的占位文字修改 placeholder 样式
::marker匹配列表项的小圆点/序号自定义 list 标记
::backdrop匹配全屏元素背后的背景层调整 dialog 遮罩

每个伪元素的渲染规则都不太一样,下面逐个拆开讲,我尽量把容易踩的坑也一并说了。

4. 高频伪元素逐个拆解:作用、属性与实战

4.1 ::before 与 ::after:最常用的“左右护法”

这两个是最常被使用的伪元素,但很多人只是“看着眼熟”,真要写的时候却总在细节上翻车。先说最基本的规则:::before和::after必须加上content属性,哪怕content: ""这样空的也行,否则伪元素根本不会在页面上渲染出来。这个content就像是伪元素的生命开关,忘了写它,后面所有属性都白搭。

我说个自定义 checkbox 的例子,这个技能点在业务里经常用到:

<label class="custom-checkbox"> <input type="checkbox" /> <span class="box"></span> 我已阅读用户协议 </label>
.custom-checkbox .box { position: relative; display: inline-block; width: 16px; height: 16px; border: 1px solid #999; border-radius: 3px; } .custom-checkbox input:checked + .box::after { content: ""; width: 10px; height: 6px; border-left: 2px solid #fff; border-bottom: 2px solid #fff; transform: rotate(-45deg); position: absolute; top: 2px; left: 2px; }

实际开发中常用的“对勾”,就是用::after画一个带左、下边框的小方块,旋转 45 度形成的。重点在于:伪元素默认是 inline 的,如果你直接给它设width和height会发现没效果,因为 inline 元素不响应宽高。所以要么在伪元素里写display: block或inline-block,要么利用 position 布局让宽高生效。这个坑我在带新人的时候几乎每期都会提到,因为出错后的表现非常隐蔽——明明写了宽高,边框怎么都不出来。

另一个要注意的点是 CSS 的层叠和定位。伪元素如果结合position: absolute使用,它的定位参照是最近的“定位祖先元素”。所以如果你想让::after出现在父容器右下角,请确保父容器已经设置了position: relative,否则它就可能飘到相对视口甚至更远的位置。

4.2 ::first-letter 与 ::first-line:排版增强

这两个伪元素是“块级元素专用”,作用于文本排版层面,跟::before/::after的“插入节点”思路完全两码事。

::first-letter就是“第一个字符”的样式选择器。最适合做杂志那种“首字下沉”效果。经典代码如下:

.article::first-letter { font-size: 48px; font-weight: bold; float: left; margin-right: 8px; line-height: 1; }

首字下沉在纸媒排版里很常见,在网页里用::first-letter实现比手动包一个<span>干净太多。

::first-line则匹配的是“第一行文本”。注意,这里的“第一行”不是指你在 HTML 里写死的换行,而是浏览器实际渲染出来的第一行。所以同一个 HTML,不同的屏幕宽度会导致::first-line选中的文本范围不同,这个特性在实现“首行缩进”或者“首行特殊背景色”时很好用。

这里有个隐藏限制:::first-line和::first-letter能设置的属性是有限制的,不是所有 CSS 属性都能生效。比如::first-line就只对字体、颜色、背景等文本相关属性起效,不能给它加padding或margin。如果你发现写了一些属性没反应,先怀疑是不是“伪元素属性白名单”的问题,不要盲目加!important。

4.3 ::selection 与 ::placeholder:状态感十足的伪装者

::selection控制的是“用户用鼠标选中文本时的样式”。以前浏览器默认选中的效果是蓝底白字,现在我们完全可以自定义成符合产品气质的配色风格:

::selection { background: #ffd54f; color: #333; }

这个细节虽然小,但在做品牌站、官网时,能明显地提升整体质感。需要提醒的坑有两点:一是不同浏览器对::selection的属性支持不一致,比如 Firefox 早期版本要求加-moz-前缀,现代版本已经不需要;二是别给::selection设置太浅的背景色,否则白字在浅色背景上看不清。说回用户体验,选中文字后高亮的配色,也是网站细节分的一部分。

::placeholder则是修改输入框占位文字的样式。以前我们得用伪类:placeholder,现在规范统一改成伪元素::placeholder,但某些老浏览器还是只认旧写法。建议在写全局样式时,两个都写上做兼容:

input::placeholder { color: #bbb; } input::-webkit-input-placeholder { color: #bbb; }

其实现在标准浏览器里::placeholder已经够用,但 Safari 老版本还需要::-webkit-input-placeholder兜底,做项目时建议用 Autoprefix 工具自动处理,省心很多。这里提醒一下:::placeholder并不是所有的 CSS 属性都支持,它主要支持颜色、字体相关的属性,像 border 和 padding 修改在很多浏览器上是无效的。

4.4 ::marker 与 ::backdrop:被低估的细节处理

::marker是控制列表项前那个小圆点或序号的。以前为了样式统一,大家喜欢写list-style: none再额外用::before加小圆点,现在可以直接改::marker:

li { list-style: none; } li::marker { color: red; font-size: 1.5em; }

不过注意:::marker不是所有属性都支持,目前能改的主要是颜色、字体、内容等有限属性,不要想着用它做复杂的布局。它更适合做“轻量级”的列表序号定制。

::backdrop很多人不熟,它在dialog元素全屏展示或者元素调用requestFullscreen()时出现。简单说,它就是那个“全屏背后的遮罩层”。如果你用原生<dialog>做弹窗并加了showModal(),默认会有一个半透明遮罩,想改这个遮罩的背景色或模糊程度,就能用:

dialog::backdrop { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(4px); }

现在原生弹窗在业务中越来越多地投入使用,与其自己写遮罩组件,不如先用原生方案配合::backdrop调样式,代码量小很多,性能也更好。当然如果项目兼容老旧浏览器,还是得继续用自研弹窗组件。

5. content 属性与高级玩法

5.1 content 的丰富取值:字符串、attr()、计数器

写伪元素不难,难的是把content玩明白。content不只是能填字符串,它还能读取 HTML 属性、动态计数,甚至拼接引号。最常见的进阶写法是从元素的属性里取值,比如做一个“图片 alt 信息展示”:

<div class="pic">.pic::after { content: attr(data-tip); position: absolute; bottom: 10px; left: 10px; background: rgba(0,0,0,0.6); color: #fff; padding: 4px 8px; border-radius: 4px; }

attr()函数可以帮我们直接把 HTML 上的>.section { counter-reset: chapter; } .section h2::before { counter-increment: chapter; content: "第 " counter(chapter) " 章:"; color: #2c7; }

这样每个.section子元素的 h2 都会自动递增编号,完全不用手动往 HTML 里写 “第一章、第二章”。对于文档站、帮助中心这类需要大量自动编号的内容,这个特性非常实用。也可以根据需要做二级编号,比如counter(chapter) "." counter(section)。

5.2 伪元素可以加动画吗?可以,但有条件

伪元素本质上是个“虚拟的盒子”,所以只要能作用在普通元素上的 CSS 属性,大都能作用在伪元素上,包括过渡和动画。比如做一个“鼠标悬停时下划线从左往右生长”的效果:

.link { position: relative; text-decoration: none; } .link::after { content: ""; position: absolute; left: 0; bottom: -2px; width: 100%; height: 2px; background: #3b82f6; transform: scaleX(0); transform-origin: left; transition: transform 0.3s ease; } .link:hover::after { transform: scaleX(1); }

这就是典型的“用伪元素做交互动画”。这里要注意,动画属性(transition/animation)作用在伪元素上是可以生效的,但前提是伪元素始终处于可渲染状态。如果你在初始状态写了display: none,那动画永远不会触发,因为 hover 时display从none变成block的过程并不支持过渡动画。想隐藏它,最好的办法是始终保留内容,用透明度、缩放或剪裁来隐藏。

另外,content属性的值是不参与过渡动画的,也就是你不能在::before里做文字的淡入淡出切换。如果想做文字切换,建议用 CSS 变量配合伪元素的 background 或 mask 等方案。

6. 实用案例拆解:从需求到代码

6.1 清除浮动:一个过了气但永远经典的场景

现在用 Flex/Grid 之后,其实很多浮动场景已经不存在了,但是有些邮件模板、打印机样式、老项目兼容还是离不开浮动。清除浮动的传统方案之一就是::after加空内容:

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

这应该是::after最广为人知的用法了。它做的事情是:在浮动元素的父容器末尾生成一个不可见的块级空元素,把它放在所有浮动元素之后,从而撑起父容器的高度。这个原理理解了之后,你去看很多老项目的样式表会非常通透。

6.2 气泡对话框的小尾巴

聊天界面里的气泡箭头,用::before或::after就可以实现。核心思路是画一个旋转 45 度的正方形,然后再用另一个伪元素覆盖掉一半,做成一个“缺口”:

.bubble { position: relative; background: #fff; border: 1px solid #ddd; border-radius: 8px; } .bubble::before { content: ""; position: absolute; left: -7px; top: 12px; width: 12px; height: 12px; background: #fff; border-left: 1px solid #ddd; border-bottom: 1px solid #ddd; transform: rotate(45deg); }

这里有个视觉效果上的细节:一定要让三角形的背景色和气泡背景色一致,边框色也和气泡边框一致,这样组合起来才像是一个整体。如果你用纯色背景,那这个技巧很简单;如果气泡是渐变色,就需要把伪元素也做成渐变或者用遮挡方案,会麻烦一些。

6.3 未读消息小红点

这个需求在移动端 H5 里非常高频。一个简单的红点,无需给后端加字段、无需在 DOM 里写一个<span>,直接用 CSS 伪元素:

.icon-badge { position: relative; font-size: 24px; } .icon-badge::after { content: attr(data-badge); position: absolute; top: -6px; right: -10px; min-width: 18px; height: 18px; line-height: 18px; padding: 0 4px; background: #ef4444; color: #fff; font-size: 12px; text-align: center; border-radius: 9px; box-shadow: 0 0 0 2px #fff; }

这里的>.card-badge { position: relative; } .card-badge::before { content: "HOT"; position: absolute; top: 8px; right: -28px; background: #f97316; color: #fff; font-size: 12px; font-weight: bold; padding: 4px 28px; transform: rotate(45deg); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); }

利用transform: rotate(45deg)加超出容器的一部分,就能做出经典的丝带角标效果。这种方案的好处是,角标的文字、颜色、显隐完全可以在一个类里控制,想给某个卡片加“HOT”标,加个类名就行。

7. 常见问题与排查实录

7.1 伪元素不显示:先检查这四件事

伪元素失效或者不渲染,是最常见的问题。我把排查顺序整理成一个列表,你按这个顺序看,基本都能定位到问题:

  1. 检查content属性是否存在。没有content的::before/::after就像没有炮弹的炮管,必然什么都不显示。
  2. 检查选择器优先级。如果常规元素选择器设置了display或background,伪元素设置后被覆盖,容易看成“没生效”。
  3. 检查伪元素是否有display: block或配合定位设置了宽高。inline 状态下的宽高是不生效的。
  4. 检查父元素是否有overflow: hidden或z-index问题导致被裁剪、被遮挡。

有一次在生产环境遇到过一个问题:某个图片上的标签用::after实现,结果在部分手机浏览器上死活不显示。后来排查发现,是有个全局样式写了img::after { display: none !important },这是别人为了禁用图片伪元素加的。我花了大半天才找到这个“隐形杀手”。所以排查的时候用 DevTools 直接查看“元素 -> 伪元素”面板,看看浏览器是否识别了这个伪元素、计算样式是否被覆盖,通常比盲猜快得多。

7.2 伪元素无法触发点击事件:接受它

伪元素是“看得见摸不着”的。它虽然能展示内容,但它不是一个真实的 DOM 节点,无法绑定 JavaScript 事件。如果你想点击一个用::before画出来的图标,然后触发某个行为,你会发现绑定事件根本没反应。

解决方案是按实际情况选择:如果必须响应事件,就把点击区域扩大到父元素,然后判断点击坐标是否在伪元素的可视范围内;在微信小程序或跨端渲染环境里,也常有类似的限制。我的经验是:交互性要求高的元素还是写成真实 DOM,千万别为了省标签硬用伪元素。

7.3 content 中的中文与特殊字符引出的编码问题

在content里直接写中文,绝大多数情况下是没问题的。但是如果你要填充特殊符号,比如引号、箭头、星号,建议直接用转义后的 Unicode:

.icon-star::before { content: "\2605"; color: #fbbf24; }

\2605就是黑色实心五角星的 Unicode 码点。用 Unicode 的好处是避免文件编码问题,也方便统一管理和替换。像箭头\2192、勾号\2713这类符号,我都建议写成编码而不是直接粘贴字符,省得到时候被文件转码搞出一堆“乱码方块”。

7.4 伪元素层级问题的解决方案

伪元素默认的层叠顺序和元素本身的子元素有关。如果父元素和伪元素还用了position: absolute,它们的渲染层级很容易变得不可控。解决思路是:显式设置z-index,并且让伪元素的 z-index 数值低于内容层但高于背景层。例如:

.card { position: relative; } .card-content { position: relative; z-index: 2; } .card::before { position: absolute; z-index: 1; }

记住一个原则:伪元素想当“背景”用,就往 z-index 低处走;想当“前景”用,就要特别关注它和真实内容之间的层级关系。

8. 浏览器兼容性要点梳理

伪元素在主流现代浏览器中的兼容性已经很好了,但“很好”不等于“无差别”。特别是老版本浏览器、部分国产浏览器的某些内核,或者 WebView 的版本较低时,伪元素的表现差异比普通属性更大。

列一个兼容性经验表,方便直接参考:

伪元素ChromeFirefoxSafariIE/Edge 老版本注意点
::before/::after支持支持支持IE8+ 单冒号必须写content
::first-line支持支持支持IE6+属性受限
::first-letter支持支持支持IE6+属性受限
::selection支持支持支持IE9+老 Firefox 需前缀
::placeholder支持支持支持需内核前缀属性受限
::marker支持支持支持不支持支持属性少
::backdrop支持支持支持不支持随 dialog 原生使用

兼容性处理的手段包括:给旧浏览器加带前缀的写法,或者使用 PostCSS 的 autoprefixer 自动生成。这里再提醒一次:不要为了兼容 IE11 而大量使用单冒号写法,容易和新版规范冲突。最好是明确项目要支持的浏览器范围,再决定写法。

在我做过的项目里,一个比较典型的兼容性坑是::placeholder在 Firefox 上的行高表现和 Chrome 不一致。即便是同一套样式,Firefox 的占位文字可能偏上或偏下。解决办法是给::placeholder单独设置line-height,并做一次跨浏览器视觉巡检。这种细节问题,靠规范不能说清楚,就是要多刷多测。

9. 我在真实项目中的一点心得

说了这么多,我个人体会最深的一点是:伪元素选择器看起来门槛很低,但真正写出“可维护的伪元素”并不容易。这背后其实要求你对“哪个样式挂在真实 DOM 上、哪个样式挂在虚拟节点上”有一个非常清醒的判断。很多新同学喜欢把装饰性内容全部塞进伪元素,导致后期想要调整某个图标时,必须改 CSS 而不是 HTML,反而增加了修改成本。

我现在给自己的一个约定是:如果一个装饰元素需要被 JavaScript 读取状态、绑定事件,或者它的内容会经常变化,那就老老实实给它一个真实的 DOM 节点;如果它只是纯粹的静态视觉装饰,比如一个图标、一条分割线、一个小红点、一个角标,那就放心交给伪元素。这个原则帮我减少了很多不必要的返工,也让 CSS 代码的职责更清晰。

最后再分享一个我亲测好用的小技巧:在调试伪元素的时候,打开 DevTools 的 Styles 面板,勾选“显示伪元素”相关选项(不同版本浏览器位置不太一样),就能直接看到::before和::after的盒子模型。你可以像调试普通元素一样,查看它们的尺寸、边框、外边距,这比靠猜和刷新页面效率高太多了。伪元素使用得当,代码会非常优雅,但前提是你真的理解它的边界在哪里。希望这期笔记能帮你把“伪元素”这个工具箱里的每一把工具都用对地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询