I used this technique for a grid of images where I needed to color-code them, possibly using more than one color for each image, without being too obtrusive.

Here’s what it looks like:

<div class="item blue"></div>
<div class="item purple"></div>
<div class="item blue purple"></div>
.item.blue::before {
  content: "●";
  color: #0074D9;

.item.purple::after {
  content: "●";
  color: #B10DC9;

To add two dots without needing additional HTML elements, we use the pseudo-elements ::before and ::after and set each one’s content property to , the black circle Unicode character (U+25CF), which makes for a good dot.

For three or more colors, we need a different approach:

<div class="item green blue purple"></div>
.item::after {
  color: transparent;
  background-clip: text;
  -webkit-background-clip: text;

.item.green.blue.purple::after {
  content: "●●●";
  background-image: linear-gradient(
    to right, #2ECC40 33%, #0074D9 33%, #0074D9 67%, #B10DC9 67%

This time we use a single pseudo-element (::after) and set its content to three dots instead of one. To color them, we use a linear-gradient with hard stops placed just between each dot (at 33% and 67%), essentially faking three separate solid colors. Since CSS gradients apply to backgrounds instead of text, we use background-clip: text to apply it to the text dots instead. All this because we can’t use multiple ::after pseudo-elements (yet).

You’ll notice, however, that right now this only works for a single color combination (green, blue, purple). If you only add one or two of those classes, you’ll get nothing. This is because every possible color combination (gradient) needs to be specified. Since this can be very tedious, I wrote the following Python script to automate it:

#!/usr/bin/env python3
import sys
from itertools import combinations

def gradient(*colors):
    g = []
    l = len(colors)
    for i, color in enumerate(colors):
        if i > 0:
            g.append(f'{color} {round(i * 100/l)}%')
        if i < l - 1:
            g.append(f'{color} {round((i+1) * 100/l)}%')
    return ', '.join(g)

prefix = sys.argv[1]
codes = [
    (n, c) for n, c in (a.split(':') for a in sys.argv[2:])

for i in range(1, len(codes) + 1):
    for combination in combinations(codes, i):
        classes, colors = zip(*combination)
        selector = ''.join(f'.{c}' for c in classes)
        dots = '●' * len(combination)
        background = (
            f'background-image: linear-gradient(to right, {gradient(*colors)})'
            if len(combination) > 1 else f'background-color: {colors[0]}'
            f'{prefix}{selector}::after {{',
            f'  content: "{dots}";',
            f'  {background};',
            sep='\n', end='\n\n'

To get the combinations used for my example, save the script as dots-css and run it like so:

dots-css .item green:#2ECC40 blue:#0074D9 purple:#B10DC9

If you’ve got N colors, this will generate 2N - 1 CSS rules, so keep that in mind.