1. 项目概述
1.1 从一个让我头疼的列表样式说起
先讲个实际经历。前段时间我在做一个数据报表页面,后端返回一长串分类数据,我直接用无序列表渲染。产品经理看了一眼说:“这个项目符号太小了,颜色也和整体风格不搭。”我打开控制台,选中那个小圆点,发现浏览器把它渲染成一个叫marker的东西。当时我下意识就想:这玩意儿能直接改样式吗?
答案是能。CSS 里专门有一个伪元素叫::marker,就是用来控制列表项前面的那个标记的。你可能早就用过list-style-type来切换实心圆点、空心圆点、数字或者字母,但::marker能做的事情远不止“换形状”这么简单,它可以让你直接给这个标记设置字体、颜色、大小,甚至替换成自定义内容。这篇文章就把我对::marker的完整理解整理出来,从基础概念到实际应用场景,再到那些容易踩的坑,一次性讲清楚。
1.2 ::marker 到底解决什么问题
要理解::marker,得先说清楚它出现的背景。在 CSS 2.1 时代,列表标记的样式控制非常弱。你能用的就是list-style-type(决定标记的形状)和list-style-position(决定标记在内容外还是内容内),仅此而已。想要一个红色的、加粗的、字号比正文大一圈的项目符号?你只能把所有<li>里的文字包一层<span>,然后用::before自己画一个符号。如果列表还要换行缩进,那对齐问题能让人折腾一晚上。
::marker是 CSS Pseudo-Elements Level 4 规范和 CSS Lists and Counters Module Level 3 中定义的一个伪元素,它表示列表项前面的那个标记盒子。有了它,你不需要修改 HTML 结构,也不需要额外包裹元素,直接用选择器就能命中标记本身:字号、颜色、字体、加粗、斜体、间距,甚至用content属性把默认的圆点换成任意文本或符号。
一句话总结:::marker把“列表标记”从一个不可控的系统默认值,变成了一个完全可操控的样式对象。
1.3 这个知识适合谁看
如果你是个刚学 CSS 不久的前端新手,这篇内容能让你少踩好多坑,因为::marker的语法和注意事项跟常规伪元素差别不小。如果你已经写了好几年 CSS,但对它的了解只停留在“能改颜色”这个层面,那文中关于content替换、嵌套列表、浏览器兼容性的部分,大概率能给你一些新思路。即使你是做富文本编辑器、组件库、文档站这类需要精细控制排版的人,::marker也是你工具箱里一个很值得常备的工具。
2. ::marker 的核心机制拆解
2.1 标记盒子是怎么生成的
要深入理解::marker,得先弄明白“标记盒子”这个概念。浏览器的渲染引擎在处理一个列表项(<li>)时,会生成一个叫“标记盒子”的东西,专门用来放list-style-type指定的符号或数字。这个盒子默认是行内级的,紧跟在内容盒子的前面。
关键点是:默认样式其实是通过::marker来设置的。你没看错,浏览器内部就是用一个 UA 样式(User Agent Stylesheet)来渲染标记。大致相当于:
li::marker { content: disc; color: initial; font-family: initial; font-size: initial; white-space: pre; }所以::marker并不是一个“新加的样式层”,它只是把浏览器原本就在用的那个机制,开放给了开发者。这也是为什么很多属性在::marker上表现很特殊——因为它们本质上是 UA 样式的一部分,你是在覆盖浏览器的默认行为,而不是从零创建元素。
2.2 支持设置哪些属性
::marker并不是所有 CSS 属性都支持。能设置的属性范围是有明确规定的,最多的是“字体类”和“文本类”属性,少部分“排版类”属性也有效。下面是我整理的支持清单:
| 属性类别 | 支持情况 | 示例 |
|---|---|---|
| 字体属性 | 支持font-size、font-family、font-weight、font-style、font-variant、font-stretch | font-size: 24px; font-weight: 700; |
| 颜色与背景 | 支持color,不支持background | color: #e67e22; |
| 文本属性 | 支持white-space、text-transform、text-decoration、text-combine-upright | text-decoration: underline; |
| 对齐方向 | 支持direction、unicode-bidi、text-align | direction: rtl; |
| 其他 | 支持content、animation、transition | content: "› "; |
注意,list-style-type设置的值在::marker里主要是影响content的默认值,而content一旦被显式设置,就会覆盖原来的符号。
2.3 content 属性和 list-style-type 的优先级关系
这里是最容易搞混的地方。::marker的content属性的优先级,高于list-style-type。
ul { list-style-type: square; } li::marker { content: "⭐ "; color: #f1c40f; font-size: 1.2em; }在上面这段代码里,list-style-type: square不会生效,因为::marker的content把默认的方块符号替换成了五角星。反过来,如果你没有设置content,那list-style-type就决定了标记显示什么。
这个特性给了开发者一个很大的自由度:即使列表用的是disc或decimal,你也可以通过content完全替换成任意字符,甚至是一段文本。
3. 实操环节:从基础到进阶
3.1 最简单的玩法:改颜色和字号
如果你只需要调整标记的颜色或大小,那是最省事的。直接选中::marker,设置color和font-size就行。
.custom-list { list-style: none; padding: 0; } .custom-list li::marker { content: "◆"; color: #3498db; font-size: 1.4em; }这里我先把ul的list-style设为none,再通过content手动指定一个菱形符号。之所以要这么做,是因为list-style: none并不影响::marker的content设置——content是覆盖在默认符号之上的。如果你不写list-style: none,::marker的content也能直接覆盖默认符号,所以这一行不是必须的,但我习惯写上,防止某些浏览器在content失效时兜底显示默认圆点。
font-size: 1.4em这个值需要注意:它继承的是列表项的字号,而不是列表容器的字号。如果列表项里的文字大小不同,标记大小也会跟着变化。
3.2 对有序列表做定制:数字样式的深度改造
有序列表(<ol>)的::marker是真正能发挥价值的地方。默认的数字样式只能靠list-style-type切换,比如decimal、decimal-leading-zero、lower-alpha。但通过::marker,你可以把数字彻底“装修”一遍。
.toc-list { list-style: none; counter-reset: toc; padding: 0; } .toc-list li { counter-increment: toc; padding-left: 32px; position: relative; } .toc-list li::marker { content: counter(toc) "."; font-weight: 700; color: #2c3e50; } /* 注意:这里不能用 ::marker 定位,所以用 ::before 做位置控制 */ .toc-list li::before { content: ""; position: absolute; left: 0; top: 0.4em; width: 20px; height: 20px; border-radius: 50%; background: #2c3e50; }这里有个关键点要说明:::marker是不能被position: absolute定位的。规范明确规定position属性在::marker上是无效的。所以如果你想把标记放在某个具体坐标上,还是需要借助::before或把标记放在内容流中。上面的例子用content: counter(toc) "."生成带序号的文本,再用::before做视觉上的圆底,实际效果是“圆形底+序号文本”。
如果你不需要圆底,其实可以简化成:
.toc-list li::marker { content: counter(toc) ". "; font-weight: 700; color: #2c3e50; font-variant-numeric: tabular-nums; }font-variant-numeric: tabular-nums会强制数字使用等宽变体,这样两位数序号和一位数序号对齐时不会抖动。对目录、步骤列表这类场景很关键。
3.3 用 ::marker 做“无 DOM 修改”的自定义符号
有些场景下你不想在 HTML 里加额外的图标元素,比如<i class="icon"></i>这种。::marker可以做成一个轻量替代方案,特别是符号以“文本”而非“图片”为主的场景。
.feature-list { list-style: none; padding-left: 0; } .feature-list li::marker { content: "✓ "; color: #27ae60; font-weight: 800; }这段代码会在每个列表项前面生成一个绿色的对勾。因为是文本符号,所以不需要引入 SVG 或 icon font,而且颜色、字号都能直接通过color和font-size控制,不需要额外处理fill或stroke之类的 SVG 属性。如果要换图标,直接改content里那个字符即可。
不过要注意:content里的文本默认不能换行,只能是一行内展示。如果你想要两行内容,需要显式使用"\A"转义字符配合white-space: pre,但这样做在多数浏览器里的渲染效果并不稳定,我一般不建议这么做。
3.4 与动画和过渡结合的玩法
::marker支持transition和animation,这是很多开发者没注意到的地方。你可以让列表标记在悬停时变化,视觉效果很细腻。
.hover-list li { transition: padding-left 0.2s ease; } .hover-list li::marker { content: "→"; color: #95a5a6; font-size: 1.1em; transition: color 0.2s ease, transform 0.2s ease; } .hover-list li:hover::marker { color: #e74c3c; transform: translateX(3px); }这里要小心:::marker里设置transform: translateX(3px),在部分浏览器里可能不生效。因为transform并不在::marker的“推荐支持属性”列表里。规范里允许保留对它的支持,但浏览器之间实现不一致。我实测 Chrome 下可以生效,Firefox 部分版本对transform支持不完整,Safari 则时好时坏。
所以稳妥的做法是:动画优先选择color和font-size这类确定支持的属性;如果一定要做位移,用padding-left过渡或者换::before来做。
4. 真实项目中的坑与排查
4.1 为什么我的 ::marker 设置了 content 却没反应
这是最常见的坑。很多人会写了这样的代码:
li::marker { content: ">>"; }结果页面上还是原来的圆点。出现这种情况,八成是你没有把列表项的list-style设为none,同时还在某个地方设置了list-style-type。虽然content理论上优先级更高,但不同浏览器对 UA 样式的处理方式有细微差别,尤其在某些重置样式表存在的情况下。
我的排查顺序是:
- 打开 DevTools,选中列表项,看
::marker是否被命中。 - 确认
content里是否包含空格或非法字符——content: ">>"本身没问题,但如果是content: >>(没有引号),那就无效。 - 确认有没有更高的优先级选择器覆盖了你的
content,比如.container li::marker和li::marker之间的优先级差异。 - 最兜底的做法:把
list-style: none加上,再把content写上。
如果以上都检查过了还不行,那就是浏览器实现问题了。可以试试在::marker上设置display: list-item,这个属性在某些老版本浏览器里能触发标记的重新渲染,属于玄学修复,但有时真能管用。
4.2 ::marker 和 ::before 的 key 信息重叠问题
你可能看到过一些教程用::marker生成序号,同时又用::before画背景圆形。但如果你不把::before的内容清空,它会和::marker产生重叠。
.info-list li { position: relative; padding-left: 28px; } .info-list li::marker { content: "①"; } .info-list li::before { content: ""; position: absolute; left: 0; top: 0; width: 20px; height: 20px; }此时::before的content为空字符串,但它仍然占据了一个空盒子。如果position: absolute没生效(比如父元素没有设置position: relative),这个空盒子会留在文本流中,把::marker挤到一边。所以写这种组合样式时,务必检查父元素的定位上下文。
另外,::marker的位置不能完全用margin或padding精细控制。规范规定::marker能够接受margin属性,但实际渲染中不同浏览器对此支持差异巨大。一个更可靠的方案:把::marker的content设置为空格占位,再用::before画真正的标记。
.clean-list li::marker { content: ""; } .clean-list li::before { content: "●"; margin-right: 8px; }这样::marker变成一个透明占位,视觉符号完全由::before控制,定位和间距都稳定得多。
4.3 嵌套列表的情境
嵌套列表在::marker里容易出问题。假设结构是:
<ul> <li>一级 A <ul> <li>二级 A1</li> <li>二级 A2</li> </ul> </li> <li>一级 B</li> </ul>如果你的选择器写的是li::marker,那么一级和二级的全部列表项都会命中。想区分层级,用li > ::marker这种写法是不合法的,因为::marker不是普通的子元素。正确做法是:
ul li::marker { content: "●"; } ul ul li::marker { content: "○"; }注意:ul ul li这个选择器匹配的是“作为某个ul内部li的li”,也就是嵌套层级的列表项。虽然不够优雅,但各种重置样式表里基本都是这么写的。如果你不希望嵌套列表里的标记被改变,可以在外层的ul上设置list-style: none,这样嵌套的ul默认就不会显示标记,除非你显式指定。
4.4 浏览器兼容性笔记
::marker的兼容性整体已经很好了。Chrome 86+、Firefox 68+、Safari 11.1+ 都支持。但有几个细节仍然要注意:
- 早期 Safari 版本对
content的支持不完整,content: "★"可能在 Safari 里不生效。 - Firefox 对
font属性的支持较晚才补全,有些版本设置font-weight: bold有效,但font-family无效。 - 移动端的 WebView 内核版本差异较大,很多老 App 的 WebView 内核停留在 Chromium 80 左右,这时
::marker的content替换功能可能不稳定。
我的经验是:如果能用::before替代的实现,就不要依赖::marker做关键视觉元素。::marker更适合做“锦上添花”的增强,比如颜色、字号、简单符号替换,这类增强在不支持时不会破坏整体布局。
5. 实战案例:一个组件库风格的步骤列表
5.1 场景设计
假设你要做一个“安装引导列表”,显示三步操作:
- 下载安装包
- 解压到指定目录
- 运行安装程序
产品要求:序号数字要醒目,数字前有圆点装饰,每个步骤悬停时数字变红。同时,HTML 结构保持最简,不套多个无语义的<span>。
5.2 最终实现代码
<ol class="steps-list"> <li>.steps-list { list-style: none; counter-reset: step-count; padding: 0; margin: 0; } .steps-list li { counter-increment: step-count; position: relative; padding: 12px 0 12px 46px; font-size: 16px; line-height: 1.5; border-bottom: 1px solid #eee; } .steps-list li:last-child { border-bottom: none; } .steps-list li::before { content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 32px; height: 32px; border-radius: 50%; background: #f0f4f8; display: flex; align-items: center; justify-content: center; } .steps-list li::marker { content: counter(step-count, decimal-leading-zero); font-weight: 700; font-size: 14px; color: #2c3e50; }这里我做了几个取舍:
- 圆底用
::before画,因为::marker里没法设置background。 - 数字内容用
::marker的content动态生成,利用counter(step-count, decimal-leading-zero)实现“01、02、03”这种补零效果。 ::before用了display: flex来让内容居中,但这个方式在::before里是可行的,因为它不是::marker,没有属性限制。
悬停效果就很简单了:
.steps-list li:hover::marker { color: #e74c3c; transform: scale(1.1); }实测在 Chrome 下transform: scale(1.1)能生效,但 Firefox 下悬停时只有颜色变化,数字不会变大。要统一效果,可以把font-size从14px改成14px并配合transition,或者直接在悬停时调整font-size:
.steps-list li::marker { content: counter(step-count, decimal-leading-zero); font-weight: 700; font-size: 14px; color: #2c3e50; transition: font-size 0.2s ease, color 0.2s ease; } .steps-list li:hover::marker { color: #e74c3c; font-size: 18px; }这样在 Firefox 里也能正常过渡,虽然效果不是“放大”而是“变大字号”,但视觉上差异不大。
5.3 为什么不直接用 ::before 显示数字
你可能会问:既然::before都能控制背景和位置,为什么不干脆连数字也用::before生成?原因是content: counter(step-count)在::before里也能用,那确实可以做到完全统一。但我仍推荐用::marker来做数字部分,理由有三点:
一是语义正确。::marker就是列表标记,屏幕阅读器在读取列表项时,会更倾向于把::marker里的内容识别为“列表标记”,而不是“普通文本内容”。虽然目前辅助技术对这一点的支持还不统一,但语义上更合理。
二是天然避开::before的定位冲突。::before需要position: absolute+top: 50%+transform才能垂直居中,但如果列表项有不同行高,这个居中逻辑容易出偏差。而::marker作为原生标记,默认就是紧贴文本行的,不需要额外定位。
三是代码更简洁。不用在::before的content里写一长串counter(),也不用担心content和背景的间距问题。
6. 对新热词“marker github”的延伸解读
6.1 关于 marker 这个名称的歧义
最近搜索“marker”相关热词时,marker github和marker安装的热度不低。这里要说明一个容易混淆的点:CSS 的::marker和 GitHub 上的“Marker”项目不是一回事。后者是一个 Python 实现文档格式转换(主要是 PDF 转 Markdown)的开源工具。虽然两者共享 “marker” 这个单词,但完全无关。
如果你在搜 CSS 技巧时看到标题里写着“Marker 安装”之类的内容,多半是遇到了同名项目。要分辨很容易:涉及::marker的 CSS 博客通常带伪元素、列表样式、content这类关键词;而 GitHub 上那个 marker 工具,核心关键词是PDF转Markdown、深度学习、文档解析。
6.2 不要把 CSS 的 ::marker 和编程中的“标记符”概念混淆
在编程领域,另一个高频词是 “marker”,比如数据结构里的“哨兵节点”(sentry node),流的“结束标记”(end-of-stream marker),或者正则里的断言标记。这些跟 CSS 的::marker没有关系。如果你带着“marker 是什么”这个疑问搜索,需要先明确你问的是哪一层:
- 前端 CSS 场景:搜
::marker 伪元素。 - JavaScript 二进制流场景:搜
stream marker或end-of-stream。 - 文档转换工具场景:搜
marker PDF converter。
在写这篇博文时,我把三类场景都简单核实了一遍,确保内容目标读者没有混淆。
7. 总结一下实操经验
7.1 什么时候优先用 ::marker
根据我自己的实际体感,::marker最适合的场景有两个特征:一是标记只是简单的文本或符号,不需要背景图片或复杂形状;二是需要保留列表的语义结构,不想在里面包裹额外元素。
典型场景包括:目录序号、步骤编号、清单符号、图例列表、树形结构的层级符号。这些场景里::marker能直接用content设置符号,并且配合counter()生成动态编号,HTML 结构保持干净。
7.2 什么时候别用 ::marker
反过来,如果你的视觉设计里需要有背景色块、圆角、渐变、图标图片,或者标记需要绝对定位到某个自由位置,那就别硬用::marker。它的能力边界很清晰:不能设置background,不能设置position,不能设置padding和margin(各浏览器支持不一致)。这时候老老实实回到::before方案,或者把标记做成独立元素,反而更可靠。
7.3 最后再分享一个细节
我在写列表时经常遇到“列表项文字被标记挤到下一行”的问题。这通常是list-style-position: inside导致的。但切换成outside之后,长文本换行时的对齐又很怪。我建议的做法是:
.list-item { list-style: none; padding-left: 0; } .list-item li { position: relative; padding-left: 1.5em; } .list-item li::marker { content: "•"; font-size: 1.2em; }这样标记仍然由::marker生成,视觉上跟正文在同一行,但通过padding-left预留出足够空间。如果想微调标记与文字之间的间隙,可以在content里加一个空格字符,比如content: "• "(两个空格)。这种方式比设置margin-right可靠得多,因为后者在部分浏览器里对::marker无效。
7.4 浏览器的“双标记”问题
最后补一个容易踩的细节:如果你同时设置了list-style-type: none和::marker { content: "★" },效果取决于浏览器的优先级策略。多数浏览器会优先遵守content,所以你能看到星星;但某些老版浏览器可能会把两者都渲染出来,导致“圆点+星星”叠加的情况。
我自己排查这个问题时发现,最稳妥的处理方式是:保持::marker的content设置完整,并且在列表容器的list-style属性上统一设置为none,而不是只在其中一项上设置:
ul { list-style: none; } li::marker { content: "★"; }这样的写法能减少“双标记”出现的概率。如果你在做跨浏览器兼容测试,建议把这行list-style: none当成默认动作,而不是可选项。
7.5 关于无障碍的几点思考
::marker生成的内容是否会被屏幕阅读器读取,目前的标准态度是:它属于“装饰性内容”,辅助技术可以忽略,也可以读取,各家实现没有统一。因此如果你的文档结构需要依赖列表顺序来传达关键信息(比如“第三步打开配置文件”),不建议只依赖::marker显示步骤数字而不在文本里体现。好的做法是:
<ol class="steps-list"> <li><span class="step-label">第一步</span> 打开配置文件</li> </ol>或者直接把“第一步”写进文本。::marker的价值更多是视觉增强,而不是信息承载。
我个人实际做项目时,会先用语义化 HTML 把文本内容写完整,再用::marker做视觉上的符号美化。这样即使某个浏览器的::marker实现出了问题,或者辅助技术忽略了标记内容,核心信息也不会丢失。