Break stuff. Now.

CSS Colored Bullets

They always preach about the workarounds but never tell the caveats

January 27, 2020

Today, I had a conversation with a junior developer. We were talking about CSS, specifically styling typography. He was tasked to do one thing: color unordered list bullets brown while keeping text black. Sounds simple right? Well...

The Right Way™

::marker {
  color: blue;
  font-size: 1.2em;

The legit way to do this is to style the ::marker pseudo-element. It's specifically designed to style list bullets and numbers. However, it's currently NOT supported by all browsers.

The workarounds

Now before I proceed, I'd like to tell you that the site we're working on is a Drupal site. There are a lot of integrations in play, some of which are beyond our control. And because of that, we don't have full control over all the HTML and CSS that gets rendered. Now with that out of the way, on to the workarounds!

  <li><span>List item</span></li>

li { color: red; } /* bullet */
li span { color: black; } /* text */

One workaround is to apply color to <li>. Then wrap its contents in an element whose styles reset color back to the original color. This is because the bullet is actually tied to the <li>, meaning the color of <li> will get applied to the bullet as well.

However, HTML could come from user-generated content, CKEditor, an external source, a third-party module, etc. Not all content passes through the code of a developer. Bottom line, we don't have control over all the HTML to be able to add that wrapper element.

ul {list-style: none}
ul li::before {content: "•"; color: red}

A more brute force approach is to do generate our own bullet using ::before and content. The approach is clever but is also problematic in many, many ways - so many, I despise this approach with a passion.

Firstly, it applies to all <li> under a <ul>. Sounds fine at first, that's the goal after all. That's until you discover that list-style: none no longer works in removing the bullets. Also, your menus start growing bullets because they too are lists. If that's not all, all third-party content will also gain bullets because they don't expect this workaround on their lists.

// Selective setting
.set-bullets ul {list-style: none}
.set-bullets ul li::before {content: "•"; color: red}

// Selective unsetting
ul {list-style: none}
ul li::before {content: "•"; color: red}
.unset-bullets ul li::before {content: ""}

A workaround on top of this workaround is to scope the bullet styles, either selectively setting or unsetting them. A class would be placed in areas where bullets are expected to be added or expected to be removed. Sounds great and all, it's cascading style sheets after all.

Again, we don't have control over all the HTML, so there will be places where we can't apply the class. Additionally, you'll have to play whack-a-mole because content can pop up anywhere, especially with a CMS. Lastly, this assumes all <li> under scoping classes set/unset bullets. But when you start nesting components that mix setting and unsetting, this can turn into a wild ride.


I've been down this road before, and I've seen a lot of developers fall into the rabbit hole, only to come out defeated. They all make assumptions early on, only to have those assumptions fall apart months later, taking with them time and budget that could have been spent elsewhere.

My recommendation is to just skip it altogether.

It's a visual enhancement with very little value, whose workarounds bring more problems than they solve. When you see fellow developers go down this path, intervene before they go too deep into the rabbit hole. Also, talk your designers out of it. Educate them what HTML and CSS can and cannot do. Don't let them do silly things.

If you really have to do it, I'd suggest using ::marker and explain away to your project manager and designer that it's only supported in Firefox and Safari.