
接到一个特别典型的需求客户给的UI稿里左侧公告列表的每个条目前面都有一个小方块默认是灰色鼠标移上去之后小方块要跟着文字一起变主题色。听上去非常简单但真正动手去查CSS时你会发现给ul、li设置前缀点很简单但想给这个前缀点单独上色、再让它随hover变色并不是写一行list-style-type就能解决的。这篇文章就围绕这个高频场景把list-style-type、::marker、hover的前因后果讲透再给出几套能做到纯CSS、不用背景图的完整写法适合正在改列表样式、做自定义下拉菜单、或者刚接触列表样式的同学参考。1. 给列表前缀点上色为什么绕了一个大弯1.1 list-style-type只负责形状不负责颜色很多人第一次接触列表样式时最先学会的就是list-style-type。这个属性确实很方便ul { list-style-type: disc; /* 实心圆点 */ } ul.square { list-style-type: square; /* 实心方块 */ } ul.circle { list-style-type: circle; /* 空心圆 */ }可以设置圆点、方块、空心圆甚至是decimal数字、lower-roman罗马数字。但问题在于它只能决定标记长什么样却永远没办法只给这个标记上色。有人可能会想那给li设置color不就行了吗试过就知道color会同时作用于列表项文字和前面的标记整个条目一起变色。如果想要文字是黑色、标记是主题橙、鼠标移上去标记变红靠list-style-type加color这条路完全走不通。这也是很多前端新手卡住的地方搜索li 圆点颜色出来的老答案要么让你直接用背景图要么让你把list-style干掉、用::before重新画一个操作成本一下子高了不少。1.2 list-style-color规范里存在现实中却指望不上在CSS列表模块CSS Lists and Counters Module Level 3的规范里确实有一个list-style-color属性看名字就是专门给列表标记设置颜色的。但残酷的现实是规范归规范浏览器支持情况非常不乐观。截至我写这篇文章时主流浏览器里还没有一个对它形成完整的默认支持你在生产环境里写了这行代码大概率没有任何视觉效果。所以遇到需求时不要抱着规范里有的东西一定能用的想法。先查兼容性再决定写法。这也是我在实际项目里反复踩过的坑。1.3 ::marker的出现补上了最后一块拼图真正让给列表标记上色变成一件轻松事的是::marker伪元素。从Chrome 86、Firefox 68、Safari 11.1开始它获得了大规模可用性并且专治各种标记样式问题。它的思路和::before、::after很接近可以把它理解为列表项标记的专属选择器。你不需要改动HTML结构不需要额外标签只要配合li使用就能精准命中列表前面的那个小圆点、小方块或者数字。li::marker { color: #f60; }一行代码标记立刻和文字“分家”了。文字保持默认颜色标记变成橙色。这就是正解的入口。2. 正解::marker 结合 hover 实现变色2.1 最简写法三行CSS搞定需求里最常见的组合是默认有个颜色hover后变成另一个颜色。用::marker写起来非常干净ul { list-style-type: square; padding-left: 1.25em; } li::marker { color: #999; transition: color 0.2s ease; } li:hover::marker { color: #f60; }这段代码的意思是list-style-type: square让列表标记变成实心方块li::marker { color: #999 }默认方块颜色是灰色li:hover::marker { color: #f60 }鼠标移到当前li上时方块变成主题橙。不需要背景图不需要额外标签不需要操作::before只要目标浏览器支持::marker这个方案就是最轻量的。我建议在项目支持范围内优先用它。2.2 圆点、空心圆、自定义符号的写法方块能控制圆点当然也可以。不同list-style-type只是形状不同上色逻辑完全一样/* 实心圆点 */ ul.dot li::marker { color: #06c; } /* 空心圆 */ ul.circle-list { list-style-type: circle; } ul.circle-list li::marker { color: #06c; } /* 默认decimal数字也可以上色 */ ol.number-list li::marker { color: #06c; font-weight: 700; }这里有一个小技巧decimal编号这种有序列表::marker同样可以控制数字的颜色和字重这让步骤说明列表的视觉层级变得非常容易处理。如果你想要的不是列表符号而是●“▶”“★”这类特殊符号::marker还可以配合content属性ul.custom li { list-style-type: none; } ul.custom li::marker { content: ★; color: #f90; }把list-style-type设为none后用content自己塞一个符号进去这个写法比用::before更干净因为标记仍然占据列表项前缀的位置对齐问题少很多。2.3 老浏览器兜底list-style:none ::before虽然::marker现在支持度已经很能看但如果你维护的网站还有大量用户在用旧版浏览器尤其是旧版移动端WebView那么::before方案依然是不可替代的兜底。思路是不要列表默认标记自己画一个方块或圆点ul { list-style: none; padding-left: 0; } li { position: relative; padding-left: 1.25em; } li::before { content: ; position: absolute; left: 0; top: 0.5em; width: 8px; height: 8px; background-color: #999; transition: background-color 0.2s ease; } li:hover::before { background-color: #f60; }::before方案最大的优势是完全不受::marker兼容性影响你可以自由控制符号大小、圆角、甚至做成小图标。缺点是符号和文字的对齐需要自己调后面第四节我会专门讲对齐的问题。这里给一个选型建议场景推荐方案现代浏览器纯圆点/方块/数字符号::marker需要特殊符号且浏览器较新::marker content需要老旧浏览器兼容或符号需要复杂样式list-style:none ::before需要图片类图标仍然建议用::before background-image或mask从需求角度出发绝大多数场景在2024年已经可以放心用::marker了。3. ::marker 能改的和不能改的能力边界要认清3.1 可控制属性速查表::marker并不是万能的它能改的属性比普通元素少很多。根据规范目前它能接受这些属性属性效果color标记颜色最常用font-family、font-size、font-weight等字体类控制数字/符号的字体表现content替换标记内容Chrome 86、Safari 17等新版本支持text-transform改变英文字母或数字大小写形态white-space控制空白处理direction控制文字方向unicode-bidi控制双方向文本排序也就是说像background、padding、margin、border、width、height这些属性在::marker里是不生效的。想给标记加背景色、加边框、扩大点击区域都不应该用::marker实现这时候还是要切回::before方案。3.2 想让标记变大借助font-size有人会问如果我不只想要颜色变化还想在hover时把圆点放大一点该怎么做直接设置width和height在::marker里没用正确思路是修改font-sizeli::marker { font-size: 1.2em; transition: font-size 0.2s ease; } li:hover::marker { font-size: 1.6em; }因为::marker本质上是一个文本性质的标记字号变大符号自然就会变大。这一招尤其适合数字列表ol可以让编号在hover时显得更突出。3.3 与list-style-image的关系何时别用图片还有一种常见的“给列表加符号”方式是list-style-imageul { list-style-image: url(dot.png); }这条属性可以让每个li前面显示一张小图片。但它的缺点也很明显图片颜色是固定的想实现hover变色就意味着要准备两张图、再用li:hover去切换list-style-image麻烦且不够灵活li:hover { list-style-image: url(dot-hover.png); }这种方式在现代项目中已经不太推荐。首先图片加载多一个请求其次图片尺寸、清晰度、Retina适配都要额外处理最后纯CSS方案完全能做到同样效果维护成本更低。如果你现在的项目里还在用这种老写法可以考虑逐步替换成::marker。4. 嵌套列表、hover联动与语义保留的实战细节4.1 嵌套列表的标记颜色继承规则实际开发中多层嵌套的ul很常见比如多级菜单、目录树。很多人改了外层li::marker颜色发现内层列表的标记颜色也跟着变了或者没变完全摸不着头脑。这里的规则是list-style-type是可继承属性但浏览器的默认样式表会给不同层级设置不同的默认值。简单说如果你显式设置了ul { list-style-type: square }那么嵌套在里面的ul ul也会继承square。同样的::marker的颜色也会跟着继承。想让不同层级颜色不同可以这样写ul li::marker { color: #999; } ul li ul li::marker { color: #06c; }或者明确区分层级结构.menu li::marker { color: #999; } .menu li ul li::marker { color: #06c; }这样一级菜单的方块是灰色二级菜单的方块是蓝色视觉层级一下子就拉开了。嵌套越深你就越能体会到::marker比::before省心——因为前者不会影响li内部布局后者一旦层级多起来选择器和定位很容易写乱。4.2 hover标记和hover整行的区别有一个很容易被忽略的问题li:hover::marker和li::marker:hover有什么区别答案是::marker是伪元素它本身不参与鼠标事件的命中所以li::marker:hover这种写法不成立。你只能通过li:hover::marker来实现“鼠标移入整个列表项时标记变色”。这是合理的交互逻辑鼠标进入li的范围内标记和文字一起响应。但如果你希望“只有当鼠标正好移到标记上时才变色移到文字上不变色”那用::marker做不到。这时需要::before方案li::before { color: #999; transition: color 0.2s; } li::before:hover { color: #f60; }实际上::before也是伪元素鼠标事件照样要命中其宿主li。如果你真的需要一个独立的、可点击的小标记那就必须给标记一个真实的子元素比如lispan classdot/span文本/li。大多数业务场景不会做到这一步了解原理即可。4.3 别把语义也隐藏掉用list-style: none去掉默认标记再用::before画一个视觉符号是很多前端在自定义列表样式时的常规操作。但这里有一个语义注意点列表标记的视觉隐藏不应该影响屏幕阅读器对这是一个列表的判断。ul和li本身的语义是由HTML标签提供的去掉视觉符号并不会让屏幕阅读器以为它不是一个列表。真正会破坏体验的是给::before塞入大量无意义的content比如content: •部分读屏软件可能会把这个装饰性字符读出来变成一串“点、点、点”的噪音。如果::before只是画一个纯色方块content: 就够了不要为了显示效果在content里塞文本。能用::marker时也不要在content里塞满特殊符号保持标记简洁。5. 实测排错我踩过的几个坑与修复方式5.1 设置了color没反应先检查li的display这是::marker最常见的“假失效”。很多人写了li::marker { color: #f60 }页面毫无变化于是一通排查最后发现li被设了display: flex或者display: grid。问题在于::marker只对display: list-item的元素生效。一旦你把li改成flex容器列表标记就会消失更别提上色了。li { display: list-item; /* 默认值别随意覆盖 */ }解决办法很简单如果布局上必须用flex那::marker方案就退场改用::before画一个绝对定位的标记。如果你只是想让列表文字在li内部居中可以把flex加在li内部的某个容器上而不是直接改li的display。5.2 ::before方案的经典错位与修复::before画圆点最烦人的就是对齐。直接给li加padding-left再用绝对定位放符号是相对可控的做法li { position: relative; padding-left: 18px; } li::before { content: ; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 8px; height: 8px; border-radius: 50%; background: #999; }top: 50% transform: translateY(-50%)可以让圆点始终垂直居中不受行高变化影响。这个方案比用text-indent负值靠谱得多尤其是在多行文本换行的时候text-indent会把第二行文本也缩进很容易乱。如果你追求更稳定的方案还可以用flexgap但注意前面说的display: flex会干掉::marker所以用flex时只能配合::before而且要处理好li的display变化。5.3 从背景图方案迁移到纯CSS的注意点以前很多项目会在ul上面挂一张背景图里面画好几个小圆点然后用background-position分别定位到每个li上。这个方案又笨重又难维护换一个标记颜色就得重新切图。迁移到::marker时建议分几步走先去掉ul上的背景图样式把list-style-type设为你需要的形状给li::marker设置初始颜色对照UI稿验证位置是否符合预期补上li:hover::marker的变色逻辑顺便用transition平滑过渡如果验证后发现视觉符号和原设计差太多比如原来是小图标、现在只是一个方块那就说明这个需求已经超出了::marker的能力范围升级成::before CSS mask或者保留小图标方案而不是硬用::marker。另外提醒一句如果旧项目里大量使用list-style-image和背景图批量替换后务必检查ol的有序列表编号是否还在。有人为了刷掉ul的圆点写了list-style: none结果把ol的编号也一起弄没了这是很常见的连带bug。在我自己的项目里现在遇到列表标记变色的需求第一反应已经是::marker了。它最大的价值不是炫技而是让一份原本要用图片、切图工具和更多HTML结构才能实现的样式变成了几行干干净净的CSS。你只需要记得那个关键前提li要维持list-item浏览器版本要支持。只要这两个前提满足剩下的就是享受CSS原生能力带来的便利。如果你还在用老办法维护列表样式不妨挑一个小页面先试试感受一下不用切图的清爽。