Generating custom fallback avatars with React, Gravatar, and JSS
Tags: react, typescript
Published: 2021-02-01 17:35:20

[Gravatar](https://en.gravatar.com/site/implement/images/), or **G**lobally **R**ecognized **Avatar**, is a free service from WordPress that allows users to associate an avatar with their email address. Any site the user signed up to using the same email address can retrieve their Gravatar using a URL constructed from its [MD5 hash](https://en.wikipedia.org/wiki/MD5).

Gravatar [supports several options for fallback images](https://en.gravatar.com/site/implement/images/#default-image) for users that have not associated a Gravatar with their email:

![Gravatar default fallbacks](https://dev-to-uploads.s3.amazonaws.com/i/w6a92dsy2vwokxwfbb38.png)

However, all of these options are either static or completely randomized, so none of them are satisfactory if you want an image that’s easily identifiable as belonging to the user.

Luckily, we can take advantage of two properties of Gravatar to easily create our own customized fallbacks:
* There is a “blank” (transparent) fallback option that we can use to place our fallback _behind_ the Gravatar itself. If the user has a Gravatar, this will display on top, hiding the fallback.
* MD5 hashes have high entropy (appearance of randomness) and consist entirely of hexadecimal digits.

We can therefore grab the first 6 hex digits of the MD5 hash, convert them to a background color, select a complementary dark/light text color to ensure adequate contrast, and superimpose the user’s initials over the top.

## Stop talking and show me the code!

The data we have to work with is as follows:

```ts
export type UserData = {
    name: string
    emailMd5: string
}
```

For our purposes, we can assume `emailMd5` is generated by our app’s back end, using something like Node.js’s `crypto.createHash('md5').update(email).digest('hex')`.

Here’s our `getColorAndBackground` function:

```ts
export const getColorAndBackground = (md5: string) => {
    const matches = md5.match(/.{2}/g)!

    const [red, green, blue] = matches.map(hex => parseInt(hex, 16))

    // Formula from https://www.w3.org/TR/AERT/#color-contrast
    const luminance = (red * 0.299 + green * 0.587 + blue * 0.114) / 255

    const color = luminance > 0.6 ? '#222' : '#fff'

    return {
        background: `rgb(${[red, green, blue]})`,
        color,
    }
}
```
 
We also need a way of converting a full name into 1 to 3 initials:
 
```ts
export const getInitials = (name: string) => {
    name = name.trim()

    if (name.length <= 3) return name
 
    return name
        .split(/\s+/)
        .map(w => [...w][0])
        .slice(0, 3)
        .join('')
}
```

This works well for names in languages that use spaces between words, and it also works well for Chinese names, which are not space delimited and typically consist of 2 or 3 characters.

Next, we need the `Avatar` component itself, which can be rendered at different sizes depending on where it’s used. Here’s the component:

```ts
export const Avatar = ({
    emailMd5,
    name,
    size = 50,
}: UserData & { size?: number }) => {
    // 250px is large enough that it will suffice for most purposes,
    // but small enough that it won't require too much bandwidth.
    // We limit the minimum size to improve caching.
    const url = `https://www.gravatar.com/avatar/${emailMd5}?s=${String(
        Math.max(size, 250),
    )}&d=blank`

    const initials = getInitials(name)

    const c = useStyles({ emailMd5, size, initials })

    return (
        <div className={c.parent}>
            <div aria-hidden='true' className={c.swatch}>
                {initials}
            </div>
            <img className={c.img} src={String(url)} alt={`${name}’s avatar`} />
        </div>
    )
}
```

Finally, we need the component styles. We use [React-JSS](https://cssinjs.org/react-jss/) to dynamically create unique class names and make use of the data passed in in from the `Avatar` component:

```ts
const useStyles = createUseStyles({
    parent: ({ emailMd5, size }) => ({
        ...getColorAndBackground(emailMd5),
        position: 'relative',
        width: size,
        height: size,
        borderRadius: '50%',
        display: 'inline-flex',
        alignItems: 'center',
        justifyContent: 'center',
        boxShadow: '5px 5px 10px rgba(0, 0, 0, 0.15)',
    }),
    swatch: ({ initials, size }) => ({
        // scale the text size depending on avatar size and
        // number of initials
        fontSize: size / (1.4 * Math.max([...initials].length, 2)),
        position: 'absolute',
        fontFamily: 'sans-serif',
        userSelect: 'none',
    }),
    img: ({ size }) => ({
        position: 'absolute',
        width: size,
        height: size,
        top: 0,
        left: 0,
        borderRadius: '50%',
    }),
})
```

And that’s it! Here’s a selection of avatars generated using this method, with a couple of existing Gravatars thrown in for good measure:

![Finished avatars](https://dev-to-uploads.s3.amazonaws.com/i/hmxogmf9t99ncv8t4xfh.png)

You can explore or fork the finished item at the [live CodeSandbox demo](https://codesandbox.io/s/gravatar-fallbacks-ky1rj?file=/src/formatters.ts), which also includes a simple `Card` component to show off what the avatars look like in context.