Styling list bullets like card suits

Sep 29, 2026

When I was seven years old (as I recollect. In truth I’m not certain of my age but I was definitely around seven, and besides, that’s the age I always am whenever I remember being a child and not knowing how old I was), I got a magic trick in my Christmas stocking.

The trick is published by Marvin’s Magic, which is a decent place to get something of decent quality for an aspiring young illusionist, and it’s called Dynamic Coins™. The performer starts with a verifiably empty brass cap. But when it’s placed face down on the table, and after an enigmatic incantation is uttered, and an appropriately mysterious gesture is executed, a stack of 10p coins is revealed when the brass cap is lifted.

Product box for the Marvin's Magic Dynamic Coins trick

Probably still available at places like Hamley’s or online

Which, as long as the audience doesn’t question why you have a couple of brass caps and a brass ring they’ve never seen you carry around before, is an absolute showstopper.

After some kind indulgences from my parents, my long-suffering older brother, and my French grandparents, who, not speaking any English, did an outstanding job at convincing me I was doing great things, I was hooked. I’d hoodwinked these suckers with something I’d pulled out of an elaborate, yuletide sock only that morning. And they loved it. So began my love of magic.

I quickly graduated to card magic: there’s an abundance of material available to the would-be card manipulator; playing cards are near-universal; and, crucially, for a child who never conceived of the possibility of asking his parents for a bit more pocket money than the paltry amount that wasn’t actually enough to do anything with (I’m actually over it. I’m fine.), playing cards are cheap. To me, they are also beautiful. See, for example: the Wicked Kingdom deck.

Which is why, when reflecting on how to add some personal flourishes to my personal website, it occurred to me that the markers (bullets) for my unordered lists could be card suits. The symbols for these, conveniently, are in Unicode, which means I can use ♠ U+2660, ♥ U+2665, ♦ U+2666, and ♣ U+2663 in CSS very easily.

I thought it would be neat if the list items in my unordered lists (ul elements) looked like this:

  • And now,
  • as you can well see with your own eyes,
  • unless you’re hard of sight
  • in which case, my apologies,
  • or you happen to currently be…
  • borrowing somebody else’s eyes?!

The list markers go in sequence through spades, hearts, diamonds and clubs, and then repeat. Provided you know of CSS’s immense power, this is relatively simple to achieve using :nth-of-type(), which became widely available in 2015.1 (Though back then you’d probably have had to use list-style-image with four image files or the ::before pseudo-element.2) Now though, we can use list-style-type and a string.3

ul > li:nth-of-type(4n + 1) {
  list-style-type: '♠︎  ';
}
ul > li:nth-of-type(4n + 2) {
  list-style-type: '♥︎  ';
}
ul > li:nth-of-type(4n + 3) {
  list-style-type: '♦︎  ';
}
ul > li:nth-of-type(4n) {
  list-style-type: '♣︎  ';
}

As is often the case with CSS, this is not the only way to achieve this effect. My first instinct was to use the ::marker pseudo-element, but content is not supported in Safari at time of writing.4 So this is not possible cross-browser:

ul > li:nth-of-type(4n + 1)::marker {
  content: '♠︎ ';
}
ul > li:nth-of-type(4n + 2)::marker {
  content: '♥︎ ';
}
ul > li:nth-of-type(4n + 3)::marker {
  content: '♦︎ ';
}
ul > li:nth-of-type(4n)::marker {
  content: '♣︎ ';
}

For the purposes of my website, where I’m in charge of the content and not likely hiding individual list items, I could stop here. But after going down a rabbit hole researching ::marker (Safari does support the font-size and color properties, for instance), I discovered that CSS can count.

Let the list do the counting

CSS has a more direct way to describe the pattern: @counter-style. Each list item, even in an unordered list, already has a number behind the scenes; this rule says how to display that number. Here is what this site uses:

@counter-style card-suits {
  system: cyclic;
  symbols: '♠︎' '♥︎' '♦︎' '♣︎';
  suffix: ' ';
}

ul {
  list-style-type: card-suits;
}

cyclic means that item five returns to the first: ♠︎. One definition covers every list, however long it is. This is really nice, and it’s worth checking out the other properties you can define for @counter-style. Unsurprisingly though, there is yet a third route that would achieve the same effect, using symbols().

ul {
  list-style-type: symbols(cyclic '♠︎' '♥︎' '♦︎' '♣︎');
}

The word cyclic is doing real work here. Leave it out and the default is symbolic: after the four single suits, you’d get ♠︎♠︎, ♥︎♥︎, and so on, which is excellent for footnote symbols, which conventionally follow this sequence *, †, ‡, §, **, ††, ‡‡.5 If you need to reuse a style, control its separator, define a fallback, or give a hint about how it should be spoken, you need the full @counter-style rule. For an anonymous one-off, symbols() will be an option once browser support catches up. Firefox implemented both in 2014; @counter-style reached Chrome and Edge in 2021 and Safari in 2023, but symbols() still has limited availability.6

While these features can be used to achieve my particular effect, they aren’t merely longer and shorter ways to achieve the same thing. Understanding how they work, and their subtle differences, unlocks their various use cases, and potential gotchas.

:nth-of-type() counts element siblings in the HTML, including ones hidden with display: none. But a counter doesn’t advance for an item that isn’t displayed.7 So, if you hide the second item in a five-item list, then the :nth-of-type() selector will produce ♠︎ ♦︎ ♣︎ ♠︎ (note that the heart is missing), but a cyclic counter style will still produce the four suits in order: ♠︎ ♥︎ ♦︎ ♣︎.

Imagine you were showing a list of filtered search results. You’d probably want the visible sequence to close up. For four fixed slots where a hidden slot should retain its place, then the :nth-of-type() selector is precisely right. The question to ask yourself is what are you counting?

A plainer demonstration of the differences would be to use <ol start="3">. Using a counter style means starting at ♦︎. But the first <li> is still the first <li>, so :nth-of-type() starts at ♠︎.

There is one annoying catch. This site’s hearts and diamonds are red because I still colour the second and third <li> markers with :nth-of-type(). If I were to hide an item, the counter style would advance past it, but my colour rule would not, and I’d end up with a red spade. (Oh, the shame!) @counter-style chooses the symbol, not a different CSS colour for each symbol. Fine for my static lists; not a complete solution for a filterable list. There I’d need to keep suit and colour together in the rendered data, or use coloured image markers, rather than pretend the selector and the counter agree.

Rummaging amongst the guts of CSS specifications

Understanding this to a high degree of accuracy required going to the source. Doing so - i.e. having the knowledge, tenacity, courage and humility to do so - is, I believe, a key trait of what distinguishes a more “senior” developer, and one I’ve observed in the colleagues I’ve emulated over the years. The same trait can also manifest as digging into a library’s source code or reading Github issues, and generally resembles detective work of an incredibly dull nature.

Here’s how to recreate the journey I went on. Start at the MDN page for @counter-style, follow its “Specifications” link to CSS Counter Styles Level 3, then read the introduction before the syntax. It says that the built-in styles weren’t enough for “worldwide typography”. The suits are a toy example of a much bigger problem: representing numbering systems and conventions that don’t fit 1., a., or •. The spec’s examples include extending decimal numbering to produce 1) rather than 1., and styles for Chinese and Ethiopic numerals. It also explains why symbols() exists: a compact style you only intend to use once.8

From there, the 2015 Mozilla introduction shows what implementers thought was exciting at the time. The earlier drafts and CSS Working Group issue tracker show how the ideas evolved; browser compatibility data and the WebKit ::marker discussion show the gap between a written specification and the browser in somebody’s hand.

Tip

This reveals a route that might be worth keeping in mind when researching unfamiliar CSS features: MDN docs for the doorway → the spec for the why → issues for the arguments → compatibility data for the reality check.

I dedicate this blog post to my seven year old self, who continues to inspire me. He would not have cared to read this.

Notes

  1. MDN’s :nth-of-type() compatibility summary gives July 2015 as its Baseline date. ↩

  2. ::before typically means removing the native marker with list-style: none. Safari/VoiceOver can then lose list semantics, although non-empty generated content may restore them; Scott O’Hara recommends an explicit <ul role="list"> where the list should still be announced. WebAIM notes that screen readers may also read generated content. ↩

  3. Compatibility data for string-valued list-style-type. ↩

  4. WebKit bug 204163 tracks support for content on ::marker. Safari can style a marker’s colour without accepting new marker content. ↩

  5. See the counter-styles specification’s symbols() examples: its default is symbolic, not cyclic. ↩

  6. Browser adoption for @counter-style and MDN’s symbols() support note. Support for a feature’s core syntax does not imply support for every descriptor: for example, speak-as has its own compatibility story. ↩

  7. Selectors Level 4 defines :nth-of-type() in terms of sibling elements; CSS Lists and Counters Level 3 says display: none elements cannot increment counters. ↩

  8. The specification introduction states the typography motivation; its sections on extending styles, predefined styles, and anonymous styles give the examples and rationale. ↩