打开编辑器输入li::marker回车样式没变。我盯着屏幕想了一会儿又把::marker改成:marker还是没反应。最后翻到控制台那一刻才反应过来浏览器版本太老压根不支持这个伪元素。这就是我第一次接触::marker时的真实经历。当时为了把一个列表前面的圆点改成红色、字号调大我在li和li::before之间折腾了两个多小时后来才知道原来 CSS 里有一个专门管列表标记的伪元素叫::marker。这篇文章我就把这个伪元素从里到外讲透顺便把我在实际项目中踩过的坑、验证过的写法、排查过的诡异问题都整理出来给正在做列表样式和自定义图标的前端同学一份能直接照着抄的参考。先澄清一件事你搜“marker”的时候大概率会搜到一堆完全不相干的东西什么compressed file ended before the end-of-stream marker was reached的解压报错什么 GitHub 上那个把 PDF 转成 Markdown 的同名开源工具还有一串“marker 安装”教程。这些确实都叫 marker但它们和 CSS 的::marker没有任何关系。咱们今天说的这个::marker全称叫::marker伪元素专门负责列表项前面的那个符号——圆点、数字、三角箭头都属于它管。1. 先别急着写代码它到底选中的是哪个“标记”1.1 每个列表项背后都有一个“标记盒子”浏览器在渲染一个display: list-item的元素时并不是只画一个盒子而是生成两个一个是放内容的主盒子另一个是放符号的标记盒子。默认情况下ul里的li、ol里的li都会自动变成list-item类型所以它们都有这个标记盒子。这个标记盒子就是::marker选中的对象。你在li::marker里写的所有样式最后都会作用到这个独立的小盒子上而不是作用到li本身的文字内容。我更喜欢用一个比喻来理解它标记盒子就像货架上的电子价签主盒子是货架上的商品。以前你想改价签上的字、颜色、字体得把整个货架挪走再想办法现在你有了直接编辑价签的权限。这个权限就是::marker伪元素。1.2 为什么以前改列表符号那么费劲在::marker出现之前前端开发者想让列表符号变得好看一点大概只有三条路每一条都走得很别扭第一条路是用list-style-type。它只能选固定几种符号形状比如disc、circle、square、decimal。颜色上完全没法单独控制符号会跟着li的color走。你想让符号是红色、文字是黑色可以把li的color改成红色然后给里面的文本和子元素全部重新设一遍颜色。这操作有多蠢干过的人心里都清楚。第二条路是用list-style-image。你可以准备一张小图片当符号但图片尺寸不好控制要么太大要么太小高清屏上还容易发虚而且加载图片有延迟首屏会出现一瞬间的空白符号。第三条路是干掉列表默认符号用伪元素伪造。具体做法是li { list-style: none; }然后li::before { content: •; position: absolute; left: ...; }。这一套要做绝对定位、处理文字折行对齐、照顾不同行高维护成本非常高尤其是有多行文本的时候符号和文字的对齐简直是噩梦。所以::marker最大的价值不是多了一个选择器而是你终于不需要伪造符号了。你可以直接对那个真实存在的标记盒子下手改它的颜色、字体、大小、内容并且完全不影响列表文字本身。1.3 不是所有元素都能用 ::marker有一点必须提前说清楚::marker不是对任何元素都生效。它只能作用于display: list-item的元素。最常见的两种情况是li和summary后面第五章我会专门讲summary的玩法。如果你把一个div设置成display: list-item它也会获得一个标记盒子理论上你同样可以用::marker去修饰它。这个特性可以玩出一些有趣的结构但实际项目中用得不多知道有这么回事就行。我见过有人试图在p::marker上设置样式还跑来问为什么不生效。原因是p默认根本不是list-item没有标记盒子你给一个不存在的东西写样式浏览器只能当没看见。2. 属性白名单哪些能改、哪些改了也白改2.1 支持的和不支持的属性很多新手拿到::marker之后会下意识把它当成一个普通元素来写样式加背景、调间距、绝对定位结果发现在浏览器里纹丝不动。这不是写错了而是::marker对属性有严格的限制。::marker支持的属性基本集中在“文字表现”这一大类color符号颜色font-*字体族、字号、字重、斜体等content符号内容可以替换成任意字符串、计数器、图片white-space控制内容里的空白处理text-transform文本大小写转换unicode-bidi、direction处理从右往左排版时的符号方向animation、transition理论上可以动画化但实际效果有限text-combine-upright这个很少用到主要是竖排文字场景不支持的属性也很明确凡是涉及布局的几乎全部不支持margin、padding、background、border、position、display、width、height、flex、grid、line-height、vertical-align这些在::marker里写了也不会生效。为什么要有这种限制你可以这样理解标记盒子的位置和尺寸是由浏览器内部的列表排版逻辑决定的如果允许你随便给它设个背景、加个大 margin那整个列表的对齐算法就乱套了。规范的做法是只开放“内容编辑”的权限把“位置布局”的权限锁死。所以用::marker的正确心态是它只能让你修改符号的“容貌”不能让你改符号的“住所”。2.2 content、list-style-type、list-style-image 的优先级关系::marker的content属性一出现很多老开发者会问一句话那list-style-type还有用吗答案是有用但要看优先级。浏览器决定“某个列表项前面显示什么符号”时会按下面的顺序判断第一级是list-style-image。如果它被设置成一张有效的图片就用图片当符号。这一级的问题我之前说过图片尺寸不好控制加载状态不可控。第二级是list-style-type。没有图片时就按这个属性显示默认符号比如disc、decimal、circle。第三级是::marker的content。这一级优先级最高只要content设置了具体的值比如content: → 浏览器就会显示这个箭头完全忽略前面两级的设置。这里有一个非常容易踩的坑如果你在::marker里写了content: 空字符串默认符号也会被隐藏。因为空字符串也是一个明确的值它覆盖了默认符号。想保留默认符号就不要写content或者写成content: normal。另外即使你在li上设置了list-style: noneli::marker依然存在你依然可以用content往里面塞内容。换句话说list-style: none只是把默认符号清除并没有把标记盒子整个拆掉。这个特性特别实用比如你想用::marker做一个完全自定义的编号不需要纠结默认的decimal会不会露出来。2.3 伪元素里不能再有伪元素还有一条隐含的规则要记牢::marker内部不能再嵌套其他伪元素。也就是说li::marker::before这种写法是无效的。原因不复杂::marker本身已经是标记盒子这个“终点”的入口了它不是普通元素内部并没有再生成子元素的能力。你可以把::marker理解成一块直接贴在标记盒子上的贴纸你能改的是贴纸上的文字和颜色但不可能在贴纸里面再贴一层贴纸。这个限制带来的实际影响是如果你需要做“符号里面再塞一个圆点”“符号底部加一个背景色”这种嵌套结构::marker做不到。这时候请果断放弃回到li::before伪造符号的方案上去。我自己的经验是把这两件事分清楚能省下很多无谓的调试时间。3. 从改色到自定义编号四段能直接抄的代码3.1 给圆点换个颜色和字号最基础的用法解决的就是我开头提到的那个需求。给ul的圆点换个颜色让它比正文大一点但正文颜色保持原样li::marker { color: #e63946; font-size: 1.4em; font-weight: 700; }这里的1.4em是相对li的字号来算的也就是说列表文字如果变大符号也会跟着按比例变大这个特性在某些响应式场景里很省心。有一点要注意::marker的color默认是继承自li的。如果你没有在li上主动设置颜色那符号默认和正文一个颜色。一旦你在li::marker里写了自己的color符号颜色就独立出来了。我见过一个反例有人在li上写了color: #333又在li::marker里写color: red结果发现正文也变红了原来是选择器写错了。回头检查他写的是li:marker少了一个冒号。::marker是伪元素必须写两个冒号单冒号是给伪类用的比如:hover这种。这个错误非常隐蔽报错也不会提示只能靠眼睛看。3.2 用 content 换掉默认符号如果你不喜欢默认的圆点想把符号换成箭头、对勾、星号直接用content覆盖li::marker { content: → ; color: #2a9d8f; }有几个细节必须提醒。第一字符串里的那个空格不是装饰它决定了符号和正文之间的距离别删掉否则符号会紧贴文字。第二如果你想写多个空格直接写→ 是没用的浏览器会把连续空白折叠成一个需要写转义的空格\00a0才能保住间距。另外content还支持attr()函数。可以这样玩li[data-icon]::marker { content: attr(data-icon) ; }然后在 HTML 里给每个li加>li>ol { counter-reset: section; } ol li { counter-increment: section; } ol li::marker { content: 第 counter(section) 讲; }这个例子里我自己创建了一个叫section的计数器在li上递增然后在::marker里读取它的值。其实不自己建计数器也可以因为浏览器给每个列表项内置了一个叫list-item的计数器也就是ol的默认编号。下面这个写法就是复用默认编号ol li::marker { content: counter(list-item) . ; }看起来和默认的decimal效果一样但你可以在此基础上继续加工改变量的字体、字号或颜色做成强调效果。这里有一个必须强调的坑counter-increment不要写进::marker里。我试过把递增逻辑放在li::marker结果编号要么从 0 开始乱跳要么在某些浏览器里重复递增。原因是标记盒子的递增时机和主盒子不是一回事浏览器实现有分歧。最稳的做法是在li上做counter-increment在::marker里只做content读取。3.4 嵌套列表、跨层级细化嵌套列表的默认符号是逐级变化的外层圆点内层空心圆再内层方块。有了::marker你可以对每一级做精细控制ul li::marker { content: ● ; } ul ul li::marker { content: ○ ; } ul ul ul li::marker { content: ■ ; }这种写法能让层级结构一目了然尤其适合文档大纲、目录树这种信息密度高的场景。配合nth-child还能做出斑马符号li:nth-child(odd)::marker { content: ◆ ; color: #e76f51; }不过要小心性能::marker的content直接作用于标记盒子频繁切换内容在极长列表里还是有计算开销的。我自己在超过两千行的目录页面里测过普通改色没有压力但那种给每个列表项写content加nth-child组合的情况滚动时会有肉眼可感知的延迟。长列表场景尽量保持符号内容统一别搞太复杂的动态生成。3.5 对齐细节marker 与正文的垂直居中问题这是实战中经常出现的一个问题符号字体调大之后符号和文字在垂直方向上对不齐符号位置偏高或者偏低。先说结论vertical-align在::marker里是无效的不要花时间调它。你真正能控制的手段只有两个第一个是li的line-height第二个是::marker自己的font-size。经验做法是给li设置一个相对稳定的line-height比如1.6然后把符号的字号控制在1em到1.2em之间。这样符号不会过度撑高行框视觉上也比较居中。如果你需要符号和第一行文字对齐得更精确可以给li加padding-left通过调整列表项内部的水平位置来改善视觉关系。还有个细节::marker里写white-space: nowrap。很多人在content里放了带空格的字符串结果在某些换行环境下符号后面的空格被折叠掉符号和正文贴在一起。加上nowrap能避免这种意外。4. details/summary 的三角箭头容易被忽略的场景4.1 原来折叠面板的箭头也是 ::marker很多人不知道::marker还能作用于details和summary。你去浏览器里打开任何一个原生折叠面板会看到标题前面有一个小三角默认情况下它朝右展开以后朝下。这个三角本质上也是summary的标记盒子渲染出来的。所以你可以直接改它的颜色和内容summary::marker { content: ▶ ; color: #457b9d; font-size: 0.9em; }这一下就把折叠面板的箭头变成了你想要的样子而且不需要额外引入图标库。4.2 跨浏览器移除默认箭头然而实际项目中更多人想要的反而是“干掉”这个默认三角然后自己做一个更好看的箭头。原因是默认的三角在不同浏览器里样式不统一而且没法做过渡动画。移除默认箭头在跨浏览器上需要两条规则配合summary { list-style: none; } summary::-webkit-details-marker { display: none; } summary::marker { content: none; }我见过有人只写了summary::-webkit-details-marker { display: none; }然后在 Firefox 里打开发现箭头还在。原因就是 Firefox 走的是summary::marker的渲染路径第二个规则没有写等于没清理干净。两条规则都写上再配合list-style: none兜底基本上就能把原生箭头“清理出厂”了。4.3 根据展开/收起状态切换内容清理掉默认箭头后你可以自己造一个箭头并让它根据面板的展开状态切换summary::marker { content: → ; } details[open] summary::marker { content: ↓ ; }但这里必须提前打个预防针::marker的content切换是瞬间完成的不会平滑过渡。我今天特别强调这一点因为真的有人写了transition: content 0.3s期待看到箭头平滑旋转最后发现完全不动。transition虽然列在支持属性里但content的过渡效果在各浏览器里基本等于没有。想要平滑旋转、缩放这类动画效果正确做法是放弃::marker回到summary::before或内部span加transform的路线。4.4 一个完整的手风琴示例把上面的技术整合起来做一个干净的折叠面板div classfaq details open summary为什么订阅了还是看不到更新/summary div classcontent p先检查通知权限再看版本是否过旧最后试试清除本地缓存。/p /div /details details summary如何导出我的历史记录/summary div classcontent p在设置页找到“数据导出”点击后等待邮件链接生效。/p /div /details /div.faq details { border-bottom: 1px solid #eee; } .faq summary { list-style: none; cursor: pointer; padding: 12px 4px; font-weight: 600; } .faq summary::-webkit-details-marker { display: none; } .faq summary::marker { content: ›; font-size: 1.4em; margin-right: 4px; color: #2a9d8f; } .faq details[open] summary::marker { content: ›; } .faq details[open] summary span.arrow { transform: rotate(90deg); }注意我用span.arrow是演示“如果要旋转动画就得用 span”而::marker只负责静态符号。实际写的时候你可以在summary里放一个带箭头的span用details[open]控制它的transform旋转这是目前体验最顺滑的方案。5. 兼容性、踩坑记录与排查速查5.1 浏览器支持时间线::marker的浏览器支持情况用一句话总结是现代浏览器放心用老版本看一眼再走。浏览器支持版本大致时间Chrome862020 年 10 月Edge862020 年 10 月Firefox682019 年 7 月Safari11.12019 年 3 月iOS Safari11.12019 年 3 月从表格可以看出::marker不是新鲜玩意已经推出好几年了。如果你的项目不需要兼容特别老的环境完全可以放开用。但 Safari 在部分属性上还是有点小脾气尤其是summary::marker的content建议在目标浏览器里实际跑一遍再上线。如果你必须兼容 IE 或特别老的移动端 WebView那还是老老实实用li::before伪造符号方案就别折腾::marker了。5.2 我踩过的几个典型坑第一个坑是给::marker写背景。有人希望符号带个圆底背景在::marker里写了background: red; border-radius: 50%结果毫无反应。原因前面说过::marker不支持背景和边框视觉上只能靠文字本身做文章。第二个坑是乱用counter-increment。我在 3.3 里已经强调过::marker里写递增会导致编号错乱。举个具体例子你在ol li::marker里写了counter-increment: section然后content: counter(section)你会发现第一个编号可能直接是 2因为标记盒子在生成内容之前就已经递增了一次。不同浏览器的处理还不完全一样所以最稳的就是在li上递增。第三个坑是隐藏符号的方式。很多人想隐藏符号时第一反应是写li::marker { display: none; }但这在::marker上是无效的。正确做法是写content: none或者直接在li上写list-style: none。第四个坑是font-size过大导致行高被撑开。这在list-style-position: inside的时候尤其明显符号占的体积变大整个列表项的行距都变了。解决方式是控制符号字号同时把li的line-height固定住。第五个坑是连续空格被折叠。我前面提过content: ✓ 想拉大间距结果符号和文字之间只有一个空格的距离。要解决就得用content: ✓\00a0\00a0这种写法把普通空格换成不换行空格。5.3 问题排查速查表现象可能原因解决方式改了 marker 完全没有效果浏览器版本过老选择器写成了li:marker单冒号升级浏览器改成li::marker双冒号改了 color 之后正文文字也变色颜色写在了li上而不是li::marker上检查选择器作用范围设置了 content 但符号不显示content 值是空字符串或者内容本身是折叠空白给 content 设置实际字符隐藏了原生箭头但 summary 出现空白只用了-webkit-details-marker没清理::marker两条规则都写上大号符号把行距撑开marker 的 font-size 太大调回 1em 到 1.2em固定 li 的 line-height想加背景色但毫无反应::marker不支持 background改用li::beforelist-style: none编号从 0 开始或者跳号counter-increment 写进了 ::marker把递增移到li上这张表基本覆盖了我日常会遇到的问题。如果你碰到的情况不在这张表里优先怀疑选择器写错然后去控制台直接查一下生成的样式看::marker规则有没有被其他规则覆盖。6. 我的使用边界与资料检索建议在这几年里我把::marker的定位想得很清楚它是一个“轻量改符号”的入口。凡是只需要改符号颜色、字体、字号、内容文字的我会优先用::marker因为代码最少、语义最清晰、不会影响正文布局。一旦视觉诉求超出了文本范畴比如要给符号加个圆形底色、做选中态的背景切换、实现平滑旋转动画我就会直接放弃::marker回到li::before伪造符号的方案。这两者的边界划清楚之后列表相关的样式需求基本不会再把我卡住。如果你以后要搜资料我建议搜索框里输入CSS ::marker pseudo-element加上MDN或者caniuse这类限定词。否则很容易被开头说的那个 GitHub 同名项目干扰。顺便提一句“marker 安装”这个词大概率就是那个 PDF 工具的教程和 CSS 的::marker没有一毛钱关系看到可以关掉。最后再分享一个小技巧如果你不确定当前浏览器支持哪些::marker属性打开控制台直接跑一行测试代码把content、color、font-size单独拆开试。这个方法比查文档快而且能直接看到当前浏览器的真实表现。我自己做多列目录样式的时候就是这么一条一条试出最稳的组合的。