Avatar
Avatar patterns — initials placeholder with a deterministic solid or gradient color from a hash of the name.
Examples
Initials with hashed color
When you don't have a profile picture (new account, no upload, image 404), fall back to the user's initials on a colored disc. The color is picked deterministically from a hash of the name, so the same person always gets the same swatch — that gives the avatar real recognition value across pages and lists, instead of the visual noise a single grey placeholder produces.
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
// Curated palette: each entry pairs well with white text. Random HSL hue
// gives muddy yellows and greens; a hand-picked list keeps every avatar
// readable.
const PALETTE = [
"bg-rose-500",
"bg-pink-500",
"bg-fuchsia-500",
"bg-purple-500",
"bg-violet-500",
"bg-indigo-500",
"bg-blue-500",
"bg-sky-600",
"bg-cyan-600",
"bg-teal-600",
"bg-emerald-600",
"bg-green-600",
"bg-lime-600",
"bg-amber-600",
"bg-orange-500",
"bg-red-500",
];
// djb2 — small, well-distributed string hash. Locale-insensitive on the
// UTF-16 code units, which is what we want for a deterministic mapping.
function hash(str: string): number {
let h = 5381;
for (let i = 0; i < str.length; i++) {
h = ((h << 5) + h + str.charCodeAt(i)) | 0;
}
return h;
}
function colorFor(name: string): string {
const idx = Math.abs(hash(name.toLowerCase())) % PALETTE.length;
return PALETTE[idx];
}
// Take first code point so surrogate-pair scripts (emoji, some CJK) don't
// get sliced in half.
function firstGlyph(token: string): string {
const cp = token.codePointAt(0);
return cp ? String.fromCodePoint(cp) : "";
}
function getInitials(name: string): string {
const tokens = name.trim().split(/\s+/).filter(Boolean);
if (tokens.length === 0) return "?";
if (tokens.length === 1) return firstGlyph(tokens[0]).toUpperCase();
return (
firstGlyph(tokens[0]) + firstGlyph(tokens[tokens.length - 1])
).toUpperCase();
}
const SAMPLES = [
"Ada Lovelace",
"Grace Hopper",
"Linus Torvalds",
"Margaret Hamilton",
"Brendan Eich",
"Bjarne Stroustrup",
];
export function AvatarPlaceholderDemo() {
const id = React.useId();
const [name, setName] = React.useState("Ada Lovelace");
return (
<div className="grid w-full gap-4">
<div className="flex justify-center">
<Avatar className="size-20" aria-label={name || "Empty"}>
<AvatarFallback className={cn("text-2xl text-white", colorFor(name))}>
{getInitials(name)}
</AvatarFallback>
</Avatar>
</div>
<div className="grid gap-2">
<Label htmlFor={id}>Name</Label>
<Input
id={id}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Type a name..."
/>
</div>
<div className="flex flex-wrap items-center justify-center gap-2">
{SAMPLES.map((n) => (
<button
key={n}
type="button"
onClick={() => setName(n)}
title={n}
className="focus-visible:ring-ring/50 rounded-full outline-none focus-visible:ring-[3px]"
>
<Avatar aria-label={n}>
<AvatarFallback className={cn("text-white", colorFor(n))}>
{getInitials(n)}
</AvatarFallback>
</Avatar>
</button>
))}
</div>
</div>
);
}Notes:
- Initials. Take the first code point of the first and last whitespace-
separated token.
codePointAt(0)(notcharAt(0)) avoids slicing surrogate pairs in half — important for emoji and supplementary CJK. Single-token names get one letter; empty input gets?. - Hash. djb2 is small, branch-free, and well-distributed for short ASCII
strings. Don't use
String.prototypenumeric coercion orDate.now()— the whole point is determinism: refresh the page, see the same color. Lowercase before hashing soJane Doeandjane doecollide. - Curated palette beats random HSL. A naive
hsl(${hash % 360}, 70%, 50%)produces muddy yellows around hue 60° and unreadable cyans around 180°. Picking from a hand-picked list of ~16 Tailwind hues guarantees every avatar pairs cleanly with white text and stays in your design system. - Keep the palette
lengthcoprime with common name patterns — 16 works well; avoid 26 (alphabet length) which clusters by surname initial. - Accessibility. The colored disc is decorative; the initials inside it
carry no semantic meaning to a screen reader. Set
aria-labelon theAvatarroot with the full name, so AT announces "Ada Lovelace" rather than "A L". The visual initials stay as text content. - Color is not data. The hue is a recognition aid, not a category. Don't encode role, status, or membership in the avatar color — users will pattern-match and you'll regret it the first time someone changes their display name.
- For server-rendered or email contexts where Tailwind classes aren't
available, render the same hash to an inline
style={{ background: ... }}with the hex equivalents of the palette entries. - For something richer than initials — geometric patterns, cartoon faces, pixel identicons — see Alternatives.
Initials with gradient
Same pattern, richer swatch: instead of one palette entry, pick two and blend them into a linear gradient, with the rotation angle also derived from the hash. Same determinism (one name, always the same gradient), dramatically more visual variety — 16 colors give 16 solid swatches but 16 × 15 × 360 ≈ 86,400 distinct gradients. This is the technique behind Vercel's avatar endpoint and Mark Miro's hash-gradient explorer.
"use client";
import * as React from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
// Same curated palette as the solid-color demo, as hex: two picks combine
// into a linear gradient, so the colors need to stay vibrant next to each
// other. Random HSL pairs clash; this list has been checked pairwise.
const COLORS = [
"#f43f5e", // rose-500
"#ec4899", // pink-500
"#d946ef", // fuchsia-500
"#a855f7", // purple-500
"#8b5cf6", // violet-500
"#6366f1", // indigo-500
"#3b82f6", // blue-500
"#0284c7", // sky-600
"#0891b2", // cyan-600
"#0d9488", // teal-600
"#059669", // emerald-600
"#16a34a", // green-600
"#65a30d", // lime-600
"#d97706", // amber-600
"#f97316", // orange-500
"#ef4444", // red-500
];
function hash(str: string): number {
let h = 5381;
for (let i = 0; i < str.length; i++) {
h = ((h << 5) + h + str.charCodeAt(i)) | 0;
}
return h;
}
// Slice three independent values out of one 32-bit hash: two palette indices
// (forced to differ) and a gradient rotation in degrees.
function gradientFor(name: string): string {
const h = Math.abs(hash(name.toLowerCase()));
const i1 = h % COLORS.length;
const i2 = (i1 + 1 + ((h >> 8) % (COLORS.length - 1))) % COLORS.length;
const angle = (h >> 16) % 360;
return `linear-gradient(${angle}deg, ${COLORS[i1]}, ${COLORS[i2]})`;
}
function firstGlyph(token: string): string {
const cp = token.codePointAt(0);
return cp ? String.fromCodePoint(cp) : "";
}
function getInitials(name: string): string {
const tokens = name.trim().split(/\s+/).filter(Boolean);
if (tokens.length === 0) return "?";
if (tokens.length === 1) return firstGlyph(tokens[0]).toUpperCase();
return (
firstGlyph(tokens[0]) + firstGlyph(tokens[tokens.length - 1])
).toUpperCase();
}
const SAMPLES = [
"Ada Lovelace",
"Grace Hopper",
"Linus Torvalds",
"Margaret Hamilton",
"Brendan Eich",
"Bjarne Stroustrup",
];
export function AvatarGradientDemo() {
const id = React.useId();
const [name, setName] = React.useState("Ada Lovelace");
return (
<div className="grid w-full gap-4">
<div className="flex justify-center">
<Avatar className="size-20" aria-label={name || "Empty"}>
<AvatarFallback
className="text-2xl text-white drop-shadow-sm"
style={{ backgroundImage: gradientFor(name) }}
>
{getInitials(name)}
</AvatarFallback>
</Avatar>
</div>
<div className="grid gap-2">
<Label htmlFor={id}>Name</Label>
<Input
id={id}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Type a name..."
/>
</div>
<div className="flex flex-wrap items-center justify-center gap-2">
{SAMPLES.map((n) => (
<button
key={n}
type="button"
onClick={() => setName(n)}
title={n}
className="focus-visible:ring-ring/50 rounded-full outline-none focus-visible:ring-[3px]"
>
<Avatar aria-label={n}>
<AvatarFallback
className="text-white drop-shadow-sm"
style={{ backgroundImage: gradientFor(n) }}
>
{getInitials(n)}
</AvatarFallback>
</Avatar>
</button>
))}
</div>
</div>
);
}Notes:
- Three values from one hash.
h % Nfor the first color index,(h >> 8) % (N-1)for the offset to the second (so they never collide),(h >> 16) % 360for the angle. A 32-bit hash has more than enough entropy; no need to callhash()three times. - Hex, not Tailwind classes. The gradient is built at runtime, so the
colors have to exist as strings the CSS engine can read —
background-image: linear-gradient(...)via inlinestyle. Tailwind's JIT can't see into dynamically-built class names; trying tocntogetherfrom-[${color}]will silently fail in production. - Pair the palette, don't randomize it. Two randomly-chosen HSL colors will eventually produce a puce-and-olive avatar. A hand-picked list where every pair is known-readable keeps the output consistently attractive. If you want more variety, grow the palette — don't switch to random HSL.
drop-shadow-smon the initials gives them a faint outline that preserves contrast when the gradient passes through a lighter stop. Without it, white letters can disappear into yellows and limes.- Same accessibility rules apply:
aria-labelon theAvatarroot carries the full name; the gradient is decorative. - For SVG output (email, social share cards, OG images) build the same
gradient as an
<linearGradient>in a<defs>block and fill a<circle>or<rect>with it. That's what Vercel's avatar endpoint does — the algorithm is identical, only the rendering target changes.
Alternatives
Initials + color is the cheapest visual identity you can ship. When you want something richer — unique per user, harder to collide, or more stylistically distinctive — reach for one of these:
- Boring Avatars — six SVG variants
(
beam,marble,pixel,sunset,ring,bauhaus) derived from a name plus a palette. Drops straight intoAvatarFallbackas an SVG, keeps the same "one name, one look" property as this page's demos. - awesome-identicons — curated list of identicon libraries, algorithms, and research across many languages and visual styles. Start here before writing your own.
- hash-avataaars — feeds a hash into the Avataaars cartoon-face construction kit, so each input yields a deterministic illustrated character with varied hair, clothing, and accessories.
- blockies — the 8×8 pixel identicon style used as the default avatar in Ethereum wallets. Compact, high-contrast visual fingerprint of an address or any arbitrary string.
- Opepen standard — Zapper's per-address avatar spec combining an Opepen-style illustration with a deterministic palette. Designed for web3 identity but the algorithm is general.
- ACM 10.5555/3489212.3489338 — academic reference on visual identity hashing, for background on why these systems work (and where they fail: near-collisions for similar inputs, or large palettes where users can't distinguish two swatches).