Avatar generator
Avatars drawn from a seed, for placeholders, new accounts, demo data and comment threads. Drawn faces, geometric marks, gradient orbs with initials and pixel stamps. The same name always gives the same picture.
One of 3 components in Placeholders and generators
Variants
Details
What Avatar generator brings along, read from the registry and the source.
- Category
- Placeholders and generators
- Area
- Visuals
- Variants
- 5
- Version
- 1.0.0 · since 1.1.0
- File
- components/ui/avatar-generator.tsx
- Brings along
- utilskit
- npm packages
- None
- Languages
- Texts come through props
- Studio
- All 5 variants editable
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The GeneratedAvatarProps interface from components/ui/avatar-generator.tsx, with the comments from the code.
seedrequiredstringAny text: a name, an email or an id
styleoptionalAvatarStylefacegeometricgradientpixel
sizeoptionalnumber | stringPixels or any CSS length. Default 48.
shapeoptionalAvatarShapecircleroundedsquare
labeloptionalstringAccessible name, e.g. the person's name. Without it the avatar is decoration.
Accessibility
Read from the source of the component.
- Sets aria-label, so screen readers announce purpose and state.
- Uses the role label.
- Needs no client state. The server writes it in full, so it reads without JavaScript too.
Motion
- With reduced motion (prefers-reduced-motion) it stays still. 1 places in the code switch motion off then.
- No animations, it stands still from the start.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 275 lines
'use client';
import { useId } from 'react';
import { cn, round } from '@/lib/kit';
/*
* Avatars drawn from a seed, for placeholders, demo data, new accounts and
* comment threads. The same seed (a name, an email, an id) always gives the
* same picture, on the server and in the browser.
*
* face a drawn person: skin, hair, eyes, mouth, sometimes glasses or a beard
* geometric three or four calm shapes on a tinted square
* gradient a soft orb of theme colors with the initials
* pixel a mirrored five by five pattern, like a stamp
*
* Backgrounds and shapes come from the theme (accent, success, warning, info),
* the face itself is drawn in fixed skin and hair tones, like a picture.
*/
export type AvatarStyle = 'face' | 'geometric' | 'gradient' | 'pixel';
export type AvatarShape = 'circle' | 'rounded' | 'square';
export interface GeneratedAvatarProps {
/** Any text: a name, an email or an id */
seed: string;
style?: AvatarStyle;
/** Pixels or any CSS length. Default 48. */
size?: number | string;
shape?: AvatarShape;
/** Accessible name, e.g. the person's name. Without it the avatar is decoration. */
label?: string;
className?: string;
}
/** A 32 bit hash of a string (FNV-1a) */
export function hashSeed(s: string) {
let h = 2166136261;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h >>> 0;
}
/** mulberry32, a small public domain random generator. Same seed, same numbers. */
function random(seed: number) {
let a = seed;
return () => {
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const TINTS = ['var(--primary)', 'var(--success)', 'var(--warning)', 'var(--info)', 'var(--primary-2)', 'var(--destructive)'];
const tint = (c: string, pct: number) => `color-mix(in oklab, ${c} ${pct}%, var(--background))`;
const SKIN = ['#f6d6bd', '#ecbc96', '#d9a074', '#b97c52', '#8f5a36', '#5f3b24'];
const HAIR = ['#231a14', '#3d281a', '#6b4022', '#a8743f', '#d8b67a', '#9a9ea4', '#17171c', '#9b3f2a'];
const SHIRT = ['var(--primary)', 'color-mix(in oklab, var(--foreground) 70%, var(--background))', 'var(--success)', 'var(--warning)', 'var(--info)', 'color-mix(in oklab, var(--primary) 50%, var(--foreground))'];
export function initialsOf(seed: string) {
const words = seed.replace(/@.*/, '').split(/[\s._-]+/).filter(Boolean);
return ((words[0]?.[0] ?? '') + (words[1]?.[0] ?? words[0]?.[1] ?? '')).toUpperCase();
}
function Face({ r }: { r: () => number }) {
const pick = <T,>(a: T[]) => a[Math.floor(r() * a.length)];
const skin = pick(SKIN);
const hair = pick(HAIR);
const shirt = pick(SHIRT);
const style = Math.floor(r() * 7);
const glasses = r() < 0.28;
const beard = r() < 0.22 && style !== 3 && style !== 6;
const smile = Math.floor(r() * 3);
const eyeY = 31 + round(r() * 2, 1);
const shade = 'rgb(0 0 0 / 0.08)';
// Hair behind the head (long styles)
const back =
style === 3 ? <path d="M14 30c0-14 8-21 18-21s18 7 18 21v20c-4 2-8 3-12 3H26c-4 0-8-1-12-3Z" fill={hair} /> : style === 6 ? <path d="M40 18c8 2 12 10 11 20-1 6-3 10-7 12 2-6 2-14-4-20Z" fill={hair} /> : null;
// Hair on top
const top = [
<path key="0" d="M19 29c-1-11 6-17 13-17s14 5 13 17c-3-5-8-8-13-8s-10 3-13 8Z" fill={hair} />,
<path key="1" d="M18 30c0-12 6-18 14-18 9 0 15 6 14 18-2-3-4-5-6-6-5 3-13 4-22 6Z" fill={hair} />,
<g key="2" fill={hair}>
<circle cx="32" cy="10" r="5.5" />
<path d="M19 29c-1-10 6-16 13-16s14 6 13 16c-3-4-8-7-13-7s-10 3-13 7Z" />
</g>,
<path key="3" d="M18 31c-1-12 6-19 14-19s15 7 14 19c-4-6-9-8-14-8s-10 2-14 8Z" fill={hair} />,
<g key="4" fill={hair}>
{[20, 25, 30, 35, 40, 44].map((x, i) => (
<circle key={i} cx={x} cy={18 - Math.abs(i - 2.5) * 1.4} r="5.2" />
))}
<path d="M19 27c0-6 6-10 13-10s13 4 13 10Z" />
</g>,
<path key="5" d="M20 26c1-8 6-12 12-12s11 4 12 12c-4-3-8-4-12-4s-8 1-12 4Z" fill={hair} opacity="0.85" />,
<path key="6" d="M19 29c-1-11 6-17 13-17s14 6 13 17c-2-6-7-9-13-9s-11 3-13 9Z" fill={hair} />,
][style];
return (
<>
{back}
{/* shoulders */}
<path d="M10 64c0-11 10-17 22-17s22 6 22 17Z" fill={shirt} />
<path d="M27 46h10v5c-2 2-8 2-10 0Z" fill={skin} />
<path d="M27 49c3 1.5 7 1.5 10 0v2c-3 2-7 2-10 0Z" fill={shade} />
{/* head */}
<ellipse cx="32" cy="32" rx="13" ry="15" fill={skin} />
<ellipse cx="19.4" cy="33" rx="2.2" ry="3" fill={skin} />
<ellipse cx="44.6" cy="33" rx="2.2" ry="3" fill={skin} />
{beard && <path d="M20 34c1 9 6 13 12 13s11-4 12-13c-2 4-6 6-12 6s-10-2-12-6Z" fill={hair} opacity="0.92" />}
{top}
{/* face */}
<g fill="#2a1d16">
<ellipse cx="26.6" cy={eyeY} rx="1.5" ry="1.7" />
<ellipse cx="37.4" cy={eyeY} rx="1.5" ry="1.7" />
</g>
<path d={`M23.6 ${eyeY - 4}q3 -1.6 5.6 0M34.8 ${eyeY - 4}q2.6 -1.6 5.6 0`} stroke={hair} strokeWidth="1.3" fill="none" strokeLinecap="round" />
<path d={`M31.6 ${eyeY + 3}q-1.4 3.2 .8 3.6`} stroke="rgb(0 0 0 / 0.22)" strokeWidth="1" fill="none" strokeLinecap="round" />
{smile === 0 && <path d="M28 40.5q4 3 8 0" stroke="#7a3b2e" strokeWidth="1.4" fill="none" strokeLinecap="round" />}
{smile === 1 && <path d="M27.5 39.8q4.5 4.6 9 0Z" fill="#7a3b2e" />}
{smile === 2 && <path d="M29 41h6" stroke="#7a3b2e" strokeWidth="1.4" strokeLinecap="round" />}
<ellipse cx="23.5" cy="37.5" rx="2.2" ry="1.2" fill="#e8766a" opacity="0.18" />
<ellipse cx="40.5" cy="37.5" rx="2.2" ry="1.2" fill="#e8766a" opacity="0.18" />
{glasses && (
<g stroke="#1f1a17" strokeWidth="1.2" fill="rgb(255 255 255 / 0.12)">
<rect x="21.8" y={eyeY - 3.6} width="9.4" height="7" rx="2.6" />
<rect x="32.8" y={eyeY - 3.6} width="9.4" height="7" rx="2.6" />
<path d={`M31.2 ${eyeY - 1}h1.6`} />
</g>
)}
</>
);
}
function Geometric({ r, uid }: { r: () => number; uid: string }) {
const base = TINTS[Math.floor(r() * 4)];
const shapes = Array.from({ length: 3 + Math.floor(r() * 2) }, (_, i) => {
const c = TINTS[(Math.floor(r() * TINTS.length) + i) % TINTS.length];
const kind = Math.floor(r() * 4);
const x = round(8 + r() * 40, 1);
const y = round(8 + r() * 40, 1);
const s = round(16 + r() * 22, 1);
const rot = Math.round(r() * 4) * 45;
// Strong enough to read on the dark theme too, where tints lean toward the background
const fill = tint(c, 70 + Math.round(r() * 30));
if (kind === 0) return <circle key={i} cx={x} cy={y} r={s / 2} fill={fill} />;
if (kind === 1) return <rect key={i} x={x - s / 2} y={y - s / 2} width={s} height={s} rx={s * 0.18} fill={fill} transform={`rotate(${rot} ${x} ${y})`} />;
if (kind === 2) return <path key={i} d={`M${x - s / 2} ${y + s / 2}A${s} ${s} 0 0 1 ${x + s / 2} ${y - s / 2}V${y + s / 2}Z`} fill={fill} transform={`rotate(${rot} ${x} ${y})`} />;
return <path key={i} d={`M${x} ${y - s / 2}L${x + s / 2} ${y + s / 2}H${x - s / 2}Z`} fill={fill} transform={`rotate(${rot} ${x} ${y})`} />;
});
return (
<>
<rect width="64" height="64" fill={tint(base, 22)} />
<g opacity="0.92">
{shapes}
</g>
</>
);
}
function Gradient({ r, uid, initials }: { r: () => number; uid: string; initials: string }) {
const a = TINTS[Math.floor(r() * TINTS.length)];
const b = TINTS[(Math.floor(r() * TINTS.length) + 2) % TINTS.length];
const c = TINTS[(Math.floor(r() * TINTS.length) + 4) % TINTS.length];
const p = () => round(15 + r() * 34, 1);
return (
<>
<defs>
<radialGradient id={`${uid}a`} cx={`${p()}%`} cy={`${p()}%`} r="70%">
<stop offset="0" stopColor={a} />
<stop offset="1" stopColor={a} stopOpacity="0" />
</radialGradient>
<radialGradient id={`${uid}b`} cx={`${p() + 30}%`} cy={`${p() + 20}%`} r="65%">
<stop offset="0" stopColor={b} />
<stop offset="1" stopColor={b} stopOpacity="0" />
</radialGradient>
</defs>
<rect width="64" height="64" fill={tint(c, 60)} />
<rect width="64" height="64" fill={`url(#${uid}a)`} />
<rect width="64" height="64" fill={`url(#${uid}b)`} opacity="0.85" />
<text x="32" y="32" dy="0.36em" textAnchor="middle" fontSize="21" fontWeight="600" fill="var(--foreground)" style={{ letterSpacing: '-0.02em' }}>
{initials}
</text>
</>
);
}
function Pixel({ r }: { r: () => number }) {
const c = TINTS[Math.floor(r() * 5)];
const cells: React.ReactNode[] = [];
for (let y = 0; y < 5; y++) {
for (let x = 0; x < 3; x++) {
if (r() < 0.5) continue;
const fill = tint(c, x === 2 ? 70 : 92);
cells.push(<rect key={`${x}${y}`} x={8 + x * 9.6} y={8 + y * 9.6} width="9.6" height="9.6" fill={fill} />);
if (x < 2) cells.push(<rect key={`m${x}${y}`} x={8 + (4 - x) * 9.6} y={8 + y * 9.6} width="9.6" height="9.6" fill={fill} />);
}
}
return (
<>
<rect width="64" height="64" fill={tint(c, 12)} />
{cells}
</>
);
}
/**
* An avatar drawn from a seed.
* <GeneratedAvatar seed="Lena Hoffmann" style="face" size={56} label="Lena Hoffmann" />
*/
export function GeneratedAvatar({ seed, style = 'face', size = 48, shape = 'circle', label, className }: GeneratedAvatarProps) {
const uid = `kav${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
const r = random(hashSeed(`${style}:${seed}`));
const bg = TINTS[Math.floor(r() * TINTS.length)];
const radius = shape === 'circle' ? 32 : shape === 'rounded' ? 14 : 0;
return (
<svg
viewBox="0 0 64 64"
data-clip-ok
role={label ? 'img' : undefined}
aria-label={label}
aria-hidden={label ? undefined : true}
className={cn('shrink-0', className)}
style={{ width: size, height: size }}
>
<defs>
<clipPath id={`${uid}c`}>
<rect width="64" height="64" rx={radius} />
</clipPath>
</defs>
<g clipPath={`url(#${uid}c)`}>
{style === 'face' && (
<>
<rect width="64" height="64" fill={tint(bg, 24)} />
<Face r={r} />
</>
)}
{style === 'geometric' && <Geometric r={r} uid={uid} />}
{style === 'gradient' && <Gradient r={r} uid={uid} initials={initialsOf(seed)} />}
{style === 'pixel' && <Pixel r={r} />}
</g>
<rect width="63" height="63" x="0.5" y="0.5" rx={Math.max(0, radius - 0.5)} fill="none" stroke="color-mix(in oklab, var(--foreground) 10%, transparent)" />
</svg>
);
}
/**
* A row of generated avatars that overlap, with a count for the rest.
* Hover lifts one a little and shows the name.
* <AvatarStack seeds={['Lena Hoffmann', 'Jonas Weber', …]} max={5} />
*/
export function AvatarStack({ seeds, max = 5, size = 40, style = 'face', more, className }: { seeds: string[]; max?: number; size?: number; style?: AvatarStyle; /** Text for the rest, {n} is the number. Default +{n}. */ more?: string; className?: string }) {
const shown = seeds.slice(0, max);
const rest = seeds.length - shown.length;
return (
<ul className={cn('flex items-center', className)}>
{shown.map((s, i) => (
<li key={s} className="group/av relative -ml-2.5 first:ml-0" style={{ zIndex: shown.length - i }}>
<span className="block rounded-full ring-2 ring-background transition-transform duration-300 ease-[var(--ease-kit)] group-hover/av:-translate-y-1 group-hover/av:scale-105 motion-reduce:transition-none">
<GeneratedAvatar seed={s} style={style} size={size} label={s} />
</span>
<span className="pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2 translate-y-1 whitespace-nowrap rounded-md border border-border bg-popover px-2 py-1 text-[12px] font-medium text-foreground opacity-0 shadow-[var(--shadow-md)] transition-[opacity,transform] duration-200 group-hover/av:translate-y-0 group-hover/av:opacity-100">
{s}
</span>
</li>
))}
{rest > 0 && (
<li className="-ml-2.5 grid place-items-center rounded-full bg-muted text-[12px] font-semibold tabular-nums text-muted-foreground ring-2 ring-background" style={{ width: size, height: size }}>
{(more ?? '+{n}').replace('{n}', String(rest))}
</li>
)}
</ul>
);
}Frequent questions
How do I install Avatar generator?
With aiw add @ai-website/avatar-generator. The command puts components/ui/avatar-generator.tsx into your project, the code is yours from then on. It needs no further npm packages.
Which variants does Avatar generator have?
5 variants, Drawn faces, Geometric marks, Gradient with initials, Pixel stamp and Stack and thread.
Can an AI assistant build with Avatar generator?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Avatar generator with aiw_components and install it with aiw_add @ai-website/avatar-generator.
Can I adjust Avatar generator without code?
Yes, in the Studio. All 5 variants are editable there, texts, lists and switches. The Studio writes the matching GeneratedAvatar code to copy.