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.
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 anywaycircle: an outlined circle, hollow in the middlesquare: a filled squarenone: 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
- strawberry milk
- rainy days
dislikes
- loud noises
- spicy food
"♡ " 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
- strawberry milk
- rainy days
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.
::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
- strawberry milk
- rainy days
dislikes
- loud noises
- spicy food
list-style: none: turns off the browser's default bullet, so we start from a blank slateposition: relativeon theli: this makes thelithe "anchor" that our bullet will position itself againstpadding-lefton theli: pushes the actual text over, leaving empty space on the left for the bullet to sit in without overlapping the words::beforewithcontent: 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 belowposition: absolute; left: 0: pins the bullet to the left edge of theli, inside the space we made withpadding-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
- strawberry milk
- rainy days
dislikes
- loud noises
- spicy food
content: "": an empty string still counts as content, which is required for::beforeto render at all, we just don't want any text inside it here, only the background imagewidth/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 pagebackground-image: this is where the actual file goes, same path you'd use in an<img>tagbackground-size: contain: shrinks or grows the image to fit inside the box we defined withwidth/height, without stretching or cropping it out of shapebackground-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
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
strawberry milk
rainy days
dislikes
loud noises
spicy food
list-style: none: same as before, turns off the default bullet since our<img>is now doing that job insteaddisplay: flex+align-items: centeron theli: 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" partgap: 6px: a clean, consistent space between the image and the text, without needing margins on either one individuallywidth/heighton.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 4object-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 shapealt="": since these images are purely decorative bullets and the words next to them already say everything a screen reader needs, an emptyalttells assistive tech to skip announcing it, instead of reading "heart image" before every single item
<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;
}
- desserts
- strawberry cake
- matcha ice cream
- rainy days
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 parentmargin-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 listmargin-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
::markeron 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.