Avatar-Generator
Avatare aus einem Seed gezeichnet, für Platzhalter, neue Konten, Demodaten und Kommentare. Gezeichnete Gesichter, geometrische Zeichen, Verlaufskugeln mit Initialen und Pixel-Stempel. Derselbe Name gibt immer dasselbe Bild.
Eine von 3 Components in Platzhalter und Generatoren
Variants
Details
Was Avatar-Generator mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Platzhalter und Generatoren
- Bereich
- Visuals
- Variants
- 5
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/avatar-generator.tsx
- Bringt mit
- utilskit
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle GeneratedAvatarProps aus components/ui/avatar-generator.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
seedPflichtstringAny 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.
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle label.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 275 Zeilen
'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>
);
}Häufige Fragen
Wie installiere ich Avatar-Generator?
Mit aiw add @ai-website/avatar-generator. Der Befehl legt die Datei components/ui/avatar-generator.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Avatar-Generator?
5 Varianten, Gezeichnete Gesichter, Geometrische Zeichen, Verlauf mit Initialen, Pixel-Stempel und Stapel und Verlauf.
Kann ein KI-Assistent Avatar-Generator einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Avatar-Generator mit aiw_components und installieren sie mit aiw_add @ai-website/avatar-generator.
Kann ich Avatar-Generator ohne Code anpassen?
Ja, im Studio. Alle 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden GeneratedAvatar-Code zum Kopieren.