custom bullets for different lists

giving two lists on the same page their own bullet style, like a heart for your likes and an x for your dislikes, instead of the same dot on both.

beginner css
two lists sitting on the same page don't have to share the same bullet. below is every way i know of to give each list its own marker, starting from a one-line css tweak using shapes the browser already knows, all the way up to fully custom bullets drawn from your own images. we'll also go over nesting a list inside another one, so sub-items can get their own smaller, indented bullet too. i'll be using a likes and dislikes list as the running example throughout, but the exact same css works for any two (or more!) lists you want to visually tell apart. ♡

why both lists look the same in the first place

by default, every <ul> on a page uses the exact same browser-given bullet, a small filled circle called disc. if you write two separate lists and don't tell the browser to treat them differently, there's nothing telling it they should look different, so of course they end up identical. the fix always starts the same way: give each list its own class, so css has something specific to target.

<ul class="likes-list">
  <li>strawberry milk</li>
  <li>rainy days</li>
</ul>

<ul class="dislikes-list">
  <li>loud noises</li>
  <li>spicy food</li>
</ul>

with that in place, we can now write css rules for .likes-list and .dislikes-list separately, and each one only affects its own list. everything below builds on top of this same html, just the css changes.

quick note if you're copying styles from this site: a lot of the lists you see across these tutorials (like the step lists) already have their bullets turned off site-wide through a css reset. if your own list still shows a default dot even after trying the examples below, check whether something in your css is setting list-style back to normal further down the file, since a later rule can override an earlier one.

option 1: the built-in shapes

css ships with a small handful of ready-made bullet shapes through list-style-type. this is the fastest possible fix, though it only gets you shapes, not custom characters or colors:

.likes-list {
  list-style-type: disc;
}

.dislikes-list {
  list-style-type: square;
}

likes

  • strawberry milk
  • rainy days

dislikes

  • loud noises
  • spicy food
  • disc: a filled circle, this is what browsers use by default anyway
  • circle: an outlined circle, hollow in the middle
  • square: a filled square
  • none: removes the bullet entirely, useful once we get into custom ones below, since we'll want a blank canvas to build on

this is genuinely enough if all you need is "make these two lists look a little different from each other" and you don't care about matching your site's theme exactly. for a decorative page though, you'll probably want something cuter than a plain square, so let's keep going.

option 2: a custom character as the bullet

modern css actually lets list-style-type take any text you want, not just the built-in keywords. this means you can drop in a heart, a star, an x, basically any character or short bit of text, as the bullet itself:

.likes-list {
  list-style-type: "♡ ";
}

.dislikes-list {
  list-style-type: "✕ ";
}

likes

dislikes

  • loud noises
  • spicy food
don't forget the trailing space! notice the space inside the quotes, "♡ " and not "♡". without it, the bullet sits pressed right up against your text with no gap in between, which looks cramped.

this is my favorite option for how little code it takes, one line per list and you're done. the one thing it can't do on its own is give the bullet a different color or size from the text next to it, since it's still counted as part of the same "marker" the browser draws. that's exactly what the next option solves.

option 3: coloring the bullet with ::marker

::marker is a special selector that targets just the bullet itself, separately from the list item's text. combine it with the custom character from option 2 and you can give each list's bullet its own color, size, or even font, without touching the text at all:

.likes-list {
  list-style-type: "♡ ";
}
.likes-list li::marker {
  color: #ff8fc0;
  font-size: 1.1em;
}

.dislikes-list {
  list-style-type: "✕ ";
}
.dislikes-list li::marker {
  color: #a88b98;
  font-size: 0.9em;
}

likes

dislikes

  • loud noises
  • spicy food

for a likes/dislikes page specifically, this is usually my go-to: a pink, slightly bigger heart for likes, and a smaller, muted x for dislikes, using nothing but two small css blocks.

heads up: ::marker only accepts a limited set of properties, mainly color, font-size, font-family, and a few text-related ones. things like text-shadow, background, or transform won't work on it. if you need any of those, skip ahead to option 4.

option 4: full manual control with ::before

this last option takes a little more code, but gives you complete control over the bullet: exact size, exact position, exact spacing, even the ability to use an image and resize it however you like. it's the same technique this very site uses for the little step lists throughout these tutorials. the idea is to turn the default bullet off entirely, then draw your own using a pseudo-element:

.likes-list,
.dislikes-list {
  list-style: none;
  margin-left: 0;
}

.likes-list li,
.dislikes-list li {
  position: relative;
  padding-left: 1.4em;
}

.likes-list li::before {
  content: "♡";
  position: absolute;
  left: 0;
  color: #ff8fc0;
}

.dislikes-list li::before {
  content: "✕";
  position: absolute;
  left: 0;
  color: #a88b98;
  font-size: 0.85em;
  top: 0.15em;
}

likes

dislikes

  • loud noises
  • spicy food
  • list-style: none: turns off the browser's default bullet, so we start from a blank slate
  • position: relative on the li: this makes the li the "anchor" that our bullet will position itself against
  • padding-left on the li: pushes the actual text over, leaving empty space on the left for the bullet to sit in without overlapping the words
  • ::before with content: this is what actually draws the bullet, here it's just holding a text character like "♡". want an image bullet instead? that's option 4b, right below
  • position: absolute; left: 0: pins the bullet to the left edge of the li, inside the space we made with padding-left

i tucked a tiny top: 0.15em onto the x's ::before in the example above too, that's just a manual nudge to visually center it against the text, since different characters sit at slightly different heights by default. small adjustments like that are exactly the kind of thing this method makes easy, and that the earlier, shorter options can't really do.

option 4b: ::before with an image (the one i personally use most)

this is the same ::before trick as option 4, just with an image instead of a text character as the bullet, and it's genuinely my go-to for most of my own lists. instead of content: url(...), i set content: "" (empty, just to make the pseudo-element exist at all) and use background-image to place the picture, since it gives full control over sizing no matter what shape or dimensions the original file is:

.likes-list,
.dislikes-list {
  list-style: none;
  margin-left: 0;
}

.likes-list li,
.dislikes-list li {
  position: relative;
  padding-left: 1.6em;
  margin-bottom: 6px;
}

.likes-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.15em;
  width: 14px;
  height: 14px;
  background-image: url("/media/heart.png");
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

.dislikes-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.2em;
  width: 12px;
  height: 12px;
  background-image: url("/media/x.png");
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
}

likes

dislikes

  • loud noises
  • spicy food
  • content: "": an empty string still counts as content, which is required for ::before to render at all, we just don't want any text inside it here, only the background image
  • width / height: sets the exact bullet size in pixels, completely independent of the original image's actual dimensions, so a huge png and a tiny one both end up the same neat size on the page
  • background-image: this is where the actual file goes, same path you'd use in an <img> tag
  • background-size: contain: shrinks or grows the image to fit inside the box we defined with width/height, without stretching or cropping it out of shape
  • background-repeat: no-repeat: without this, a small image tiles itself over and over to fill the box, like a pattern, which isn't what we want for a single bullet
why i prefer this over content: url(...): both work, but content: url("/media/heart.png") renders the image at its real, original size, and some older browsers don't let you resize it from there at all. the background-image version above always lets you set an exact width/height no matter the source file's size, so i can drop in the same graphic across different lists at different scales without needing to resize the actual png each time.

about the heart and x graphics: /media/heart.png and /media/x.png are two little graphics i made myself in photoshop (shaping and editing them by hand, not pulled from somewhere else). you're welcome to use them as bullets on your own likes/dislikes page, i just ask that you please credit me properly if you do (a small link back to my site anywhere on the page is perfect). they're not f2u to be pulled and reposted without credit, so please be mindful of that. ♡

option 5: a plain <img> tag, kept small and centered with flexbox

if the math behind positioning a ::before feels like a lot, there's a simpler way that gets you the exact same result: skip pseudo-elements entirely and just place a real <img> tag inside each <li>, right before the text. this is genuinely the most beginner-friendly of all the image options, since it's a normal html element you already know, sized the normal way:

<ul class="likes-list">
  <li><img src="/media/heart.png" class="bullet-img"> strawberry milk</li>
  <li><img src="/media/heart.png" class="bullet-img"> rainy days</li>
</ul>

<ul class="dislikes-list">
  <li><img src="/media/x.png" class="bullet-img"> loud noises</li>
  <li><img src="/media/x.png" class="bullet-img"> spicy food</li>
</ul>
.likes-list,
.dislikes-list {
  list-style: none;
  margin-left: 0;
}

.likes-list li,
.dislikes-list li {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}

.bullet-img {
  width: 14px;
  height: 14px;
  object-fit: contain;
  flex-shrink: 0;
}

likes

dislikes

  • loud noises
  • spicy food
  • list-style: none: same as before, turns off the default bullet since our <img> is now doing that job instead
  • display: flex + align-items: center on the li: this is what keeps the image and the text sitting on the same line, vertically centered against each other, no matter how tall either one is. this is the "centered inline with the text" part
  • gap: 6px: a clean, consistent space between the image and the text, without needing margins on either one individually
  • width/height on .bullet-img: works exactly like resizing any other image on your site, completely independent of the file's real dimensions, this is the actual fix for the oversized bullet problem from option 4
  • object-fit: contain: keeps the image's own proportions intact while it's squeezed into that small box, so it doesn't stretch or squish out of shape
  • alt="": since these images are purely decorative bullets and the words next to them already say everything a screen reader needs, an empty alt tells assistive tech to skip announcing it, instead of reading "heart image" before every single item
option 4b vs option 5: they end up looking identical, so pick whichever feels easier to reason about. option 4b keeps your html completely clean (no image tag cluttering up each <li>), but needs position: absolute math. option 5 needs one extra tag per item, but the sizing and centering is just normal, everyday css you already know from styling any other image. i usually go back and forth between the two depending on the project.

bonus: nesting a list inside a list

sometimes one flat list isn't enough, you want to group a few related items underneath a bigger one, each group getting a smaller, indented bullet of its own. you'll see this a lot for breaking a list down into categories, or listing sub-points under a single item. the trick is simply placing a second <ul> inside an <li> of the first one:

<ul class="likes-list">
  <li>
    desserts
    <ul class="likes-sublist">
      <li>strawberry cake</li>
      <li>matcha ice cream</li>
    </ul>
  </li>
  <li>rainy days</li>
</ul>

the html alone will already look nested and indented a little by default, but let's make it match the rest of our styling, and give the sub-items a smaller, hollow bullet so they visually read as "one level down" from the parent item. that hollow, unfilled dot is actually just the circle keyword from option 1 again:

.likes-sublist {
  list-style-type: circle;
  margin-left: 1.5em;
  margin-top: 4px;
}
  • list-style-type: circle: gives the sub-list an outlined, hollow circle instead of a filled one, which is a nice visual cue on its own that it's a smaller/lesser item than its parent
  • margin-left: pushes the whole sub-list further to the right, so it sits visibly "inside" the item it belongs to instead of lining up with the parent list
  • margin-top: a small gap between the parent item's text and where its sub-list begins, purely for breathing room

you can absolutely mix this with any of the other options above too, nothing says the sub-list has to use circle specifically. a smaller version of your custom heart character, or a lighter-colored ::before bullet, works just as well, the important part is just that it reads as visually smaller/lighter than its parent, so the hierarchy still makes sense at a glance.

which one should you actually use?

  • just want the two lists to look different, don't care how: option 1, built-in shapes, one line each
  • want a cute character bullet with the least code possible: option 2
  • want that character in your own theme's colors: option 3, adding ::marker on top
  • want full control over a text-character bullet (exact spacing, positioning): option 4
  • want a full-size graphic (like mine) resized down cleanly with css: option 4b or option 5, either works, pick whichever reads easier to you

result

two lists on the same page were always allowed to look different, the browser just needs to be told they're different first, through separate classes, before any bullet styling can target one without affecting the other. from there it's really just a matter of how much control you want: a single list-style-type line for something quick, or full control over size and position, through ::before or a plain <img>, when you want your likes and dislikes to feel like two completely different little sections.