UI/UX law

// Perception & Gestalt;

Deterministic Avatar Colour

Most users never upload a profile photo, so the fallback — initials over a background colour — is what people actually see. That colour has to be derived deterministically from something stable about the person (their id or name), never picked at random and never the same flat neutral for every user, and never a value chosen once and stored in the database.

Mechanism

Why it happens

A flat grey initials chip throws away the one thing a fallback could still carry: colour. The eye's pre-attentive system separates regions by hue before anyone reads a label — exactly the shortcut a list of teammates without photos needs. Give every row the same neutral background and that shortcut disappears; the eye has nothing left to sort by, so scanning the list collapses into reading each name in turn.

The colour also has to be pinned to the person, not to the moment it's rendered. Math.random() (or any seed drawn from render order, position, or time) reassigns a fresh hue every time a row remounts, a page refreshes, or a list refetches, so the same person looks different from one view to the next. A pure hash of a stable identifier — sum the character codes of the user's id or name, reduce mod 360, feed that into hsl() — fixes this for free: identical input always produces identical output, with nothing to persist. Storing a chosen colour in the database instead just relocates the same problem into a column that can silently drift out of sync with the value it was derived from.

Impact

Why it matters

  • Most users never upload a profile photo — the fallback is the default state, not an edge case, so it earns the same design attention as a real photo would
  • A flat grey chip on every row removes the one signal (colour) that lets the eye tell people apart before reading a single name
  • Math.random() (or any per-render seed) reassigns a new hue on every reload or remount, so a user's visual identity flickers instead of staying recognisable
  • A colour stored in the database is one more column, one more migration, and one more value that can drift from the name or id it was meant to represent

Example

Without vs. with

Without
ALAda Lovelace
GHGrace Hopper
ATAlan Turing

A team list shows every teammate without a photo as the same flat grey circle with their initials, so telling Ada, Grace, and Alan apart means reading each name in turn — colour gives the eye nothing to sort by.

With
ALAda Lovelace
GHGrace Hopper
ATAlan Turing

The same list hashes each name into a hue — sum its character codes, reduce mod 360, feed that into hsl() — and uses it as the initials' background, so Ada, Grace, and Alan render as three distinct, stable colours that look the same on every reload.

Checklist

How to apply it

Fall back to initials over a photo the instant avatarUrl is null, missing, or fails to load — never a blank or plain-grey circle

Derive the background hue from a stable identifier: sum its character codes, reduce mod 360, and feed that into hsl(hue, S%, L%)

Never seed the colour with Math.random(), Date.now(), a list index, or anything else that isn't a pure function of the same input every time

Fix saturation and lightness (for example 68%/60%) and vary only the hue, so every generated colour stays equally readable against the initials text

Prefer hashing a stable id over a display name where names can change — a rename shouldn't reassign someone's colour

Compute the colour at render time from data already on hand; don't add a database column, migration, or API field just to store it

Recipe

Code example

Where it shows up

Element areas

NavigationCardsTables