May 2, 2023May 2nd, 2023 · 1 minute read · tweet
Colorful avatars
An interactive walkthrough of an algorithm to pick an avatar's color based on the user's initials.
I've written about tracking the loading state of an avatar image before. However, it's not uncommon to have users without a profile picture.
In that case, the usual approach is to show the user's initials over a colored background.
Picking the right color
Avatars with images are recognizable and memorable. Just like you might recognize someone close to you by the sound they make when they walk, avatar images become a shortcut to quickly identify a user.
When there is no image to display, achieving the same result is a bit harder. A person's initials alone are not very useful when doing a quick visual scan. That's where the color comes in, and there are a few important rules to follow:
- Deterministic outcome: it's not enough to just pick a random color, it needs to be consistent. If it was different every time, it'd be useless and confusing!
- Uniform probability distribution: one color should not show up more often than the others. Note that we're not going for perfection here, but it should be close enough.
- Disctinct permutations: different arrangements of the same letters (like "DG" and "GD") should result in different colors. If not, it'd be hard to tell them apart since they share the same letters and colors!
With these in mind, we can now build the algorithm.
Do these rules remind you of something?
If you've ever worked with hash functions, you'll notice that they have a lot in common. In fact, when I first implemented this, I called it a "color hash"!
Thanks to Vladimir Klepov (who reviewed this article) for pointing this out.
The magic number
There are many possible combinations of initials.
We have 702 potential combinations with one or two initials (), and that's just for the English alphabet (A-Z). It increases greatly when you account for other alphabets! Our mission is to turn each of these into a small number so that we can map it to a color.
For that, we can use a bit of simple modular arithmetic, but we need to turn our initials into a number first. I call this number the "magic number" because I am fancy like that.
tsTry
function getInitialsMagicNumber(initials: string) {const numbers = initials.toLowerCase().substring(0, 2).split("").map((char) => char.charCodeAt(0));return numbers.reduce((acc, n) => acc + n);}
To achieve this, we:
- "Normalize" the initials string by converting it to lowercase and taking only the first two characters.
- Split and turn both characters into numbers with String.prototype.charCodeAt.
- Finally, add them together to get the magic number.
Here's a little playground to try it out:
Mapping to a color
Let's say we have these five colors:
tsTry
const COLORS = ["purple", "orange", "green", "yellow", "blue"] as const;
We can now map the magic number to one of these colors by using the remainder (%) operator (A.K.A. modulo).
tsTry
export function getAvatarColorFromInitials(initials: string) {const magicNumber = getInitialsMagicNumber(initials);const colorIndex = magicNumber % COLORS.length;return COLORS[colorIndex];}
Try it:
With this, we've achieved our first two goals: deterministic outcome and (more or less) uniform probability distribution. We're missing the third one, though.
Disctinct permutations
Here's a demo that shows the color for both permutations of the same initials (the numbers below are the magic numbers):
As you can see, with the current algorithm, the colors are the same for both permutations. This is because the magic number is the same for both.
We can fix that with a little trick: we can add something I called "spice" (because, again, I am fancy like that) to the magic number. This spice will be a number that is either 0 or 1, depending on the order of the characters (based on their UTF-16 code unit).
For example, if we have "DG" and "GD", only one of them will be affected by the spice, so they will have different magic numbers and, therefore, different colors.
tsTry
function getInitialsMagicNumber(initials: string) {const numbers = initials.toLowerCase().substring(0, 2).split("").map((char) => char.charCodeAt(0));const spice = numbers[0] < numbers[1] ? 0 : 1;return numbers.reduce((acc, n) => acc + n) + spice;}
Now it works as intended:
And we are done!
Bonus: initials from name and one final demo
Here's another neat utility to extract initials (up to two characters) from a name:
tsTry
export function getInitialsFromName(name: string) {const matchResult = name?.matchAll(/(\S)\S*/g);const matches = [...matchResult].map((r) => r[1]);return matches ? `${matches[0]}${matches[1] ?? ""}` : undefined;}
Finally, here's a demo of the Avatar
component I built for Guide that displays all permutations of initials using a-z and a few non-English characters: