Zum Inhalt springen
UI Kit
Accent

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

5 Variants

Variants

Gezeichnete Gesichter

Für jeden Namen eine gezeichnete Person mit Haut, Haaren, Augen und Mund, manchmal mit Brille oder Bart. Neue Gesichter zeichnet einen frischen Satz, und das Team steigt neu auf.

Im Studio öffnen

Geometrische Zeichen

Ruhige Formen auf getöntem Quadrat für Arbeitsbereiche, Projekte und Teams. Gezeigt in einer Auswahl mit Haken, der der Wahl folgt.

Im Studio öffnen

Verlauf mit Initialen

Eine weiche Kugel aus Theme-Farben mit den Initialen. Tippe einen Namen, das Avatar zeichnet sich beim Tippen neu, oder wähle eines der acht darunter.

Im Studio öffnen

Pixel-Stempel

Ein gespiegeltes Muster aus fünf mal fünf Feldern, wie ein Stempel. Gut für Benutzernamen und anonyme Konten, tippe, um es wechseln zu sehen.

Im Studio öffnen

Stapel und Verlauf

Überlappende Avatare mit Zahl für den Rest, Hover hebt eines an und zeigt den Namen. Darunter ein Kommentarverlauf mit denselben Gesichtern.

Im Studio öffnen

Details

Was Avatar-Generator mitbringt, aus Registry und Quellcode gelesen.

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

Props

Die Schnittstelle GeneratedAvatarProps aus components/ui/avatar-generator.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • seedPflicht
    string

    Any text: a name, an email or an id

  • styleoptional
    AvatarStyle

    facegeometricgradientpixel

  • sizeoptional
    number | string

    Pixels or any CSS length. Default 48.

  • shapeoptional
    AvatarShape

    circleroundedsquare

  • labeloptional
    string

    Accessible 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
components/ui/avatar-generator.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert