Thoughts .toString()

Astro: Generating tag colors the easy way


One common functionality when it comes to blogs are tags, or keywords, that categorize posts and make searching easier. Because we want more fine-tuned categories and we can’t foresee every keyword that comes up in the future, it would be more convenient to go with a bottom-up approach, where the tags are defined from the posts as opposed to a central repository. With this workflow, we wouldn’t want to sap our creative juice worrying about what colors and properties to assign each tag at the same time that we are writing an article. It would be convenient, then, that tags are automatically assigned a color.

The tag color should be deterministic to avoid confusion. Hashes are suitable for this application over PRNGs since a good hash should have good uniformity and an avalanche effect. In other words, the output space should be uniform, and small changes in the input should produce a significantly different output. We wouldn’t want “C#” and “F#” to have similar colors, nor, say, “HTML” and “XML”. We also want generation to be order-invariant, so that adding new tags or changing the ordering preserves previously generated colors.

Since we’re using Astro to pre-render a static site, the tags won’t be generated in runtime, so we don’t need it to be extremely fast. But at the same time, it isn’t exactly a security concern, and the color output space isn’t so large that extremely strong hashes become necessary. With all this in mind, it’s probably worth avoiding writing our own hash function and instead use a fast or non-cryptographic hash functon.

So I did a quick Google for the fastest hash functions and found the following table.

NameSpeedQualityAuthor
xxHash5.4GB/s10Y.C.
MurmurHash 3a2.7GB/s10Austin Appleby
SBox1.4GB/s9Bret Mulvey
Lookup31.2GB/s9Bob Jenkins
CityHash641.05GB/s10Pike & Alakuijala
FNV0.55GB/s5Fowler, Noll, Vo
CRC320.43GB/s9
MD5-320.33GB/s10Ronald L. Rivest
SHA1-320.28GB/s10

Google Open Source Readme

Rather than use broken cryptographic hashes like SHA1 or MD5 like I was going to, it makes more sense to look into newer algorithms like xxHash.

In order to make tags easy to read, there needs to be sufficient contrast between the text and background color. It’s not immediately obvious how to determine the lightness of colors using the RGB model. But luckily, CSS allows us to use HSL (hue, saturation, lightness), where hue controls the color over 360 degrees, and the other axes of freedom are percentages. For the tags, we’d want every color, but for light themes we might want lower saturation and higher lightness, and for dark themes medium saturation and lower lightness. Note that high saturation causes neon colors that stand out a bit too much and should be avoided.

So the basic steps are:

  1. Set a random seed for determinate results.
  2. Generate a hash from the tag text lowercased.
  3. Use a piece of the hash to generate hues 0-359 degrees.
  4. Use another piece of the hash to generate saturations of, say, about 25-50% or so for light themes, and 35-60% for dark themes.
  5. Use another piece to generate lightnesses of about 60-85% for light themes, and 30-55% for dark themes.
  6. Define a dark gray, near-black color for text using light themes, and a near-white color for dark themes.

Finally, we npm install js-xxhash and put it all together.

/src/components/Tag.astroastro
1---
2import { xxHash32 } from 'js-xxhash';
3const seed = 0;
4
5interface Props {
6    name: string;
7}
8
Lower case to reduce duplicate tags from different casing.
9const { name } = Astro.props;
10const nameLower = name.toLowerCase();
11
Ex. If hash = 0x9f25c5a7. Then, hue = 0x9f2 % 360, saturation = 0x5c % 25, lightness= 0x5a % 25.
12let hash: string = xxHash32(nameLower, seed).toString(16);
13const hue = parseInt(hash.slice(0, 3), 16) % 360;
14const saturation = parseInt(hash.slice(3, 5), 16) % 25;
15const lightness = parseInt(hash.slice(5, 7), 16) % 25;
16
17const hslLight = `hsl(${hue}deg ${25 + saturation}% ${60 + lightness}%)`;
18const hslDark = `hsl(${hue}deg ${35 + saturation}% ${30 + lightness}%)`;
19---
20
Abbreviated styling for example.
21<a href=`/blog/tag/${nameLower}`>
22    <span style=`--tag-color-light: ${hslLight}; 
23                --tag-color-dark: ${hslDark};`>
24        {nameLower}
25    </span>
26</a>
27
28<style>
29    span {
30        @media (prefers-color-scheme: light) {
31            color: black;
32            background-color: var(--tag-color-light);
33        }
34        @media (prefers-color-scheme: dark) {
35            color: white;
36            background-color: var(--tag-color-dark);
37        }
38    }
39</style>