Skip to content
UI Kit
Accent color

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

5 variants

Variants

Drawn faces

A drawn person for every name, with skin, hair, eyes and mouth, sometimes glasses or a beard. New faces draws a fresh set and the team rises in again.

Open in Studio

Geometric marks

Calm shapes on a tinted square for workspaces, projects and teams. Shown in a workspace picker with a check that follows the choice.

Open in Studio

Gradient with initials

A soft orb of theme colors with the initials. Type a name and the avatar redraws as you type, or pick one of the eight below.

Open in Studio

Pixel stamp

A mirrored five by five pattern, like a stamp. Good for usernames and anonymous accounts, type to see it change.

Open in Studio

Stack and thread

Overlapping avatars with a count for the rest, hover lifts one and shows the name. Below, a comment thread with the same faces.

Open in Studio

Details

What Avatar generator brings along, read from the registry and the source.

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

Props

The GeneratedAvatarProps interface from components/ui/avatar-generator.tsx, with the comments from the code.

  • seedrequired
    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.

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
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>
  );
}

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.

Often searched

Jump to

Copied to the clipboard