
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-stretchfont-size: 24px; font-weight: 700;颜色与背景支持color不支持backgroundcolor: #e67e22;文本属性支持white-space、text-transform、text-decoration、text-combine-uprighttext-decoration: underline;对齐方向支持direction、unicode-bidi、text-aligndirection: rtl;其他支持content、animation、transitioncontent: › ;注意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 classicon/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 classsteps-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: absolutetop: 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 classsteps-list lispan classstep-label第一步/span 打开配置文件/li /ol或者直接把“第一步”写进文本。::marker的价值更多是视觉增强而不是信息承载。我个人实际做项目时会先用语义化 HTML 把文本内容写完整再用::marker做视觉上的符号美化。这样即使某个浏览器的::marker实现出了问题或者辅助技术忽略了标记内容核心信息也不会丢失。