Generating custom fallback avatars with React, Gravatar, and JSS

lionel-rowe - Feb 1 '21 - - Dev Community

Gravatar, or Globally Recognized 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.

Gravatar supports several options for fallback images for users that have not associated a Gravatar with their email:

Gravatar default fallbacks

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:

export type UserData = {
    name: string
    emailMd5: string
}
Enter fullscreen mode Exit fullscreen mode

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:

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

We also need a way of converting a full name into 1 to 3 initials:

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('')
}
Enter fullscreen mode Exit fullscreen mode

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:

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>
    )
}
Enter fullscreen mode Exit fullscreen mode

Finally, we need the component styles. We use React-JSS to dynamically create unique class names and make use of the data passed in in from the Avatar component:

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%',
    }),
})
Enter fullscreen mode Exit fullscreen mode

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

You can explore or fork the finished item at the live CodeSandbox demo, which also includes a simple Card component to show off what the avatars look like in context.

. . . . . . . . . . . . . .