May 2, 2023May 2nd, 2023 · 3 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.
tsfunctionOpen in playgroundfunction getInitialsMagicNumber(initials: string): numbergetInitialsMagicNumber(initials: stringinitials: string) { constconst numbers: number[]numbers =initials: stringinitials .String.toLowerCase(): stringtoLowerCase() .Converts all the alphabetic characters in a string to lowercase.
String.substring(start: number, end?: number): stringsubstring(0, 2) .Returns the substring at the specified location within a String object.
String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)split("") .Split a string into substrings using the specified separator and return them as an array.
Array<string>.map<number>(callbackfn: (value: string, index: number, array: string[]) => number, thisArg?: any): number[]map((Calls a defined callback function on each element of an array, and returns an array that contains the results.
char: stringchar) =>char: stringchar.String.charCodeAt(index: number): numbercharCodeAt(0)); returnReturns the Unicode value of the character at the specified location.
const numbers: number[]numbers.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number (+2 overloads)reduce((Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
acc: numberacc,n: numbern) =>acc: numberacc +n: numbern);}
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:
tsconstOpen in playgroundconst COLORS: readonly ["purple", "orange", "green", "yellow", "blue"]COLORS = ["purple", "orange", "green", "yellow", "blue"] astype const = readonly ["purple", "orange", "green", "yellow", "blue"]const;
We can now map the magic number to one of these colors by using the remainder (%) operator (A.K.A. modulo).
tsexport functionOpen in playgroundfunction getAvatarColorFromInitials(initials: string): "purple" | "orange" | "green" | "yellow" | "blue"getAvatarColorFromInitials(initials: stringinitials: string) { constconst magicNumber: numbermagicNumber =function getInitialsMagicNumber(initials: string): numbergetInitialsMagicNumber(initials: stringinitials); constconst colorIndex: numbercolorIndex =const magicNumber: numbermagicNumber %const COLORS: readonly ["purple", "orange", "green", "yellow", "blue"]COLORS.length: 5length; returnconst COLORS: readonly ["purple", "orange", "green", "yellow", "blue"]COLORS[const colorIndex: numbercolorIndex];}
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.
tsfunctionOpen in playgroundfunction getInitialsMagicNumber(initials: string): numbergetInitialsMagicNumber(initials: stringinitials: string) { constconst numbers: number[]numbers =initials: stringinitials .String.toLowerCase(): stringtoLowerCase() .Converts all the alphabetic characters in a string to lowercase.
String.substring(start: number, end?: number): stringsubstring(0, 2) .Returns the substring at the specified location within a String object.
String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)split("") .Split a string into substrings using the specified separator and return them as an array.
Array<string>.map<number>(callbackfn: (value: string, index: number, array: string[]) => number, thisArg?: any): number[]map((Calls a defined callback function on each element of an array, and returns an array that contains the results.
char: stringchar) =>char: stringchar.String.charCodeAt(index: number): numbercharCodeAt(0)); constReturns the Unicode value of the character at the specified location.
const spice: 0 | 1spice =const numbers: number[]numbers[0] <const numbers: number[]numbers[1] ? 0 : 1; returnconst numbers: number[]numbers.Array<number>.reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number (+2 overloads)reduce((Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
acc: numberacc,n: numbern) =>acc: numberacc +n: numbern) +const spice: 0 | 1spice;}
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:
tsexport functionOpen in playgroundfunction getInitialsFromName(name: string): string | undefinedgetInitialsFromName(name: stringname: string) { constconst matchResult: RegExpStringIterator<RegExpExecArray>matchResult =name: stringname?.String.matchAll(regexp: RegExp): RegExpStringIterator<RegExpExecArray>matchAll(/(\S)\S*/g); constMatches a string with a regular expression, and returns an iterable of matches containing the results of that search.
const matches: string[]matches = [...const matchResult: RegExpStringIterator<RegExpExecArray>matchResult].Array<RegExpExecArray>.map<string>(callbackfn: (value: RegExpExecArray, index: number, array: RegExpExecArray[]) => string, thisArg?: any): string[]map((Calls a defined callback function on each element of an array, and returns an array that contains the results.
r: RegExpExecArrayr) =>r: RegExpExecArrayr[1]); returnconst matches: string[]matches ? `${const matches: string[]matches[0]}${const matches: string[]matches[1] ?? ""}` :var undefinedundefined;}
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: