# @illlustrations/avatars

Seeded SVG avatars for JavaScript and React. Everything renders locally: no API
key, network request or account. Croods is the first style. Version 1.1.0.

Web: https://illlustrations.co/docs · Parts JSON: https://illlustrations.co/docs/parts.json · Agent skill: https://illlustrations.co/docs/skill.md

## Install

```sh
npm install @illlustrations/avatars
```

React projects need React 18.3.1 or 19.

| Import | Contents |
| --- | --- |
| `@illlustrations/avatars` | createAvatar, fromJSON and types. No React. |
| `@illlustrations/avatars/react` | The Avatar component (React 18.3+ or 19). |
| `@illlustrations/avatars/croods` | The Croods style. Future styles get their own path. |
| `@illlustrations/avatars/croods/presets.json` | 24 resolved preset states. |
| `@illlustrations/avatars/svg/croods-001.svg` | Preset SVG files, 001 to 024. |

## React

```tsx
import { Avatar } from '@illlustrations/avatars/react';
import { croods } from '@illlustrations/avatars/croods';

export function UserAvatar() {
  return (
    <Avatar
      assets={croods}
      seed="alex"
      size={64}
      shape="circle"
      background="#EDEDFF"
      title="Alex's avatar"
    />
  );
}
```

Use `title` or an accessible label for meaningful avatars; otherwise the
component is decorative. The React entry declares `'use client'` and uses
`useId` for unique SVG IDs, so it works in Next.js pages. Apps with several
React roots need matching server/client `identifierPrefix` values.

## Plain JavaScript

```ts
import { createAvatar } from '@illlustrations/avatars';
import { croods } from '@illlustrations/avatars/croods';

const avatar = createAvatar(croods, {
  seed: 'alex',
  size: 64,
  shape: 'circle',
  background: '#EDEDFF',
});

// Use an image in your page. No React required.
const image = document.createElement('img');
image.src = avatar.toDataUri();
image.alt = "Alex's avatar";
document.body.append(image);
```

For server output use `avatar.toString()`. Multiple inline SVG strings on one
page each need a unique `toString({ idPrefix })`.

## Seeds and randomness

```tsx
// Same seed, same avatar: on the server, the client and every device.
<Avatar assets={croods} seed={user.id} />

// Seeds pick head, face, outfit and glasses. Facial hair is opt-in.
<Avatar assets={croods} seed={user.id} selections={{ facialHair: 'beard' }} />

// Explicit selections always win over the seed.
<Avatar assets={croods} seed={user.id} selections={{ head: 'bun', face: 'happy' }} />
```

A seed picks one combination from a frozen, versioned pool (`v1`), so the same
seed and style version always give the same avatar. Different seeds can share
an avatar. Seeds never add facial hair; select it explicitly. Resolution order:
style defaults → seeded combination → explicit `selections`.

## Parts

| Slot | Required | Default | Valid IDs |
| --- | --- | --- | --- |
| `head` | yes | `default` | `afro-1`, `afro-2`, `bald`, `bangs`, `bowl-cut`, `braid-1`, `braid-2`, `bun-1`, `bun-2`, `bun`, `default`, `dread`, `long-hair`, `long-hair-1`, `long-hair-2`, `messy-afro`, `messy`, `mohawk`, `no-hair`, `normal`, `pixie`, `quiff-1`, `quiff-2`, `shaggy`, `short-hair-1`, `short-hair-2`, `short-hair-3`, `short-hair-4`, `short-hair-5`, `short-hair-6`, `short-hair-7`, `short-hair-8`, `short-hair-9`, `straight-long`, `trimmed`, `wavy-curls` |
| `face` | no | `normal` | `drool`, `drool-2`, `happy`, `normal`, `open-mouth-1`, `sad`, `none` |
| `upperBody` | yes | `t-shirt-1` | `blazer`, `hoodie-1`, `shirt-1`, `t-shirt-1`, `t-shirt-bag` |
| `facialHair` | no | `none` | `beard`, `moustache`, `stubble`, `none` |
| `accessories` | no | `none` | `glass`, `none` |

Seeds pick every slot except `facialHair`, which stays `none` unless you select it.

Read them at runtime:

```ts
import { croods } from '@illlustrations/avatars/croods';

const hairstyles = croods.parts.head.map(({ id, name }) => ({
  value: id,
  label: name ?? id,
}));
```

## Colors

Three layers, applied in this order:

1. `theme`: `'ink'` or `'neutral'` recolors everything.
2. `palette`: swap any artwork color by its hex, like the website builder.
3. `colors`: the four roles (`hair`, `skin`, `clothing`, `stroke`) always win.

### Color lists

Any role and `background` take a list; the seed picks one entry per person:

```tsx
// One color, or a list: the seed picks one entry per person, every time.
<Avatar
  assets={croods}
  seed={user.id}
  colors={{ clothing: ['#254CE9', '#F76549', '#77E87B'], hair: ['original', '#272727'] }}
  background={['#DDE5FF', '#FFE6D2', '#D4EDE2']}
/>
```

### Palette

```ts
import { createAvatar } from '@illlustrations/avatars';
import { croods } from '@illlustrations/avatars/croods';

croods.palette;          // every artwork color: ['#000000', '#FF0000', '#FFFFFF', …]

const avatar = createAvatar(croods, {
  selections: { upperBody: 't-shirt-bag' },
  colors: { hair: '#254CE9' },          // roles first
  palette: { '#FF4B33': '#254CE9' },    // then any artwork color: here, the bag
});

avatar.sourceColors();   // artwork colors in this avatar's parts
```

| Artwork color | What it paints |
| --- | --- |
| `#000000` | Outlines (stroke role) |
| `#FF0000` | Hair and beard (hair role) |
| `#FFFFFF` | Skin (skin role), eye whites and teeth |
| `#313130` | Dark garments (clothing role) |
| `#FF4B33` | Bag on t-shirt-bag |
| `#FFE900` | Yellow garments (clothing role), shirt under the blazer |
| `#FF3F3F` | Mouth and tongue |
| `#424242` | Mouth shadow |
| `#5DECFF` | Drool |
| `#F4F1EB` | Detail on shirt-1 |

A palette entry changes every use of that color, so change skin with `colors.skin`
rather than `#FFFFFF` (which also paints eye whites). Unknown colors throw.

## API reference

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `assets` | AvatarStyle | Required | The imported style, such as croods. React prop; for createAvatar it is the first argument. |
| `seed` | string | Default character | Repeatable part selection. Same seed and style version give the same result; different seeds can share a result. Seeds never add facial hair. |
| `selections` | object | From seed | Override head, face, upperBody, facialHair or accessories. Optional slots accept 'none'. |
| `colors` | object | Original artwork | Override hair, skin, clothing or stroke. Hex, 'transparent' or 'original', or a list the seed picks from. |
| `theme` | 'ink' \| 'neutral' | Original artwork | 'ink' uses #0040FC and white; 'neutral' uses black and white. Explicit colors override theme roles. |
| `palette` | object | None | Swap any artwork color, like the builder: { '#FF4B33': '#254CE9' }. Keys come from croods.palette. Roles win over it; it wins over theme accents. |
| `background` | string \| string[] | 'transparent' | A hex color or 'transparent', or a list the seed picks from. Separate from the artwork palette. |
| `size` | number | 64 React / 600 core | Width and height in pixels. Above 0, at most 8192. |
| `shape` | string | 'square' | 'square', 'rounded' or 'circle'. |
| `seedPool` | string | 'v1' | The versioned list of seeded combinations. Only needed to pin results across versions. |
| `title` | string | Decorative | Accessible name (React). Also accepts ARIA, className, style and SVG presentation props. |

`createAvatar(style, options)` returns `{ toString(options?), toDataUri(options?), toJSON() }`.
`toString` accepts `{ idPrefix?, title? }`; `toDataUri` accepts `{ title? }`.
`fromJSON(style, state)` validates and restores saved state.

### Errors

Invalid input throws instead of rendering something unexpected.

| Error | Cause |
| --- | --- |
| `Unknown head part: curly-hair` | A part ID that isn't in the style. Use an ID from the parts list. |
| `Unknown slot: hair` | A selection key that isn't a slot. Slots are head, face, upperBody, facialHair and accessories. |
| `Invalid hair color: use a hex color, transparent or original` | Named CSS colors like 'red' are not accepted. |
| `Size must be greater than 0 and at most 8192` | Size out of range. |
| `idPrefix must start with a letter…` | toString({ idPrefix }) got an ID starting with a digit or containing other characters. |
| `Avatar state schema or style version does not match` | fromJSON got state saved with a different style version. |
| `Unknown palette color: #ABCDEF is not in the croods artwork` | A palette key that the artwork never uses. Pick keys from croods.palette or avatar.sourceColors(). |

## Save and restore

```ts
import { createAvatar, fromJSON } from '@illlustrations/avatars';
import { croods } from '@illlustrations/avatars/croods';

const avatar = createAvatar(croods, {
  seed: 'alex',
  selections: { head: 'straight-long', accessories: 'none' },
  colors: { hair: '#254CE9' },
});

// Store the resolved parts, colors, and style version.
const saved = JSON.stringify(avatar.toJSON());

// Restore later using the matching style version.
const restored = fromJSON(croods, JSON.parse(saved));
const svg = restored.toString();
```

Saved state includes the style and schema versions. Mismatches are rejected
rather than silently changing the avatar.

### Static SVG files

The package includes 24 portraits, `croods-001.svg` to `croods-024.svg`:

```ts
import avatarUrl from '@illlustrations/avatars/svg/croods-001.svg?url';

const image = document.createElement('img');
image.src = avatarUrl;
image.alt = 'Croods avatar';
document.body.append(image);
```

`?url` is Vite-specific. In Next.js, copy the SVG to `public/` and use its URL.

## Recipes

### Profile picture fallback

Show the uploaded photo when there is one, and a stable avatar when there isn't.

```tsx
import { Avatar } from '@illlustrations/avatars/react';
import { croods } from '@illlustrations/avatars/croods';

export function UserPhoto({ user }: { user: { id: string; name: string; photoUrl?: string } }) {
  if (user.photoUrl) return <img src={user.photoUrl} alt={user.name} width={40} height={40} />;
  // Seed with the user ID, not the name: names change, IDs don't.
  return <Avatar assets={croods} seed={user.id} size={40} shape="circle" title={user.name} />;
}
```

### Member list

One theme and shape for a calm, consistent list. Decorative avatars need no title when the name is next to them.

```tsx
import { Avatar } from '@illlustrations/avatars/react';
import { croods } from '@illlustrations/avatars/croods';

export function Members({ members }: { members: { id: string; name: string }[] }) {
  return (
    <ul>
      {members.map(member => (
        <li key={member.id}>
          <Avatar assets={croods} seed={member.id} size={32} shape="circle" theme="neutral" background="#F2F0EE" />
          {member.name}
        </li>
      ))}
    </ul>
  );
}
```

### Let people choose their avatar

Build a picker from the style's part list and save the resolved state. A seed alone can't restore explicit choices.

```tsx
'use client';
import { useState } from 'react';
import { createAvatar, type AvatarState } from '@illlustrations/avatars';
import { Avatar } from '@illlustrations/avatars/react';
import { croods } from '@illlustrations/avatars/croods';

export function AvatarPicker({ userId, onSave }: { userId: string; onSave: (state: AvatarState) => void }) {
  const [head, setHead] = useState<string>();
  const options = { seed: userId, selections: head ? { head } : {} };
  return (
    <>
      <Avatar assets={croods} {...options} size={120} title="Your avatar" />
      <select value={head ?? ''} onChange={event => setHead(event.target.value || undefined)}>
        <option value="">Surprise me</option>
        {croods.parts.head.map(part => <option key={part.id} value={part.id}>{part.name ?? part.id}</option>)}
      </select>
      <button onClick={() => onSave(createAvatar(croods, options).toJSON())}>Save</button>
    </>
  );
}

// Later, render the saved state:
// <Avatar assets={croods} selections={saved.selections} colors={saved.colors}
//   theme={saved.theme} background={saved.background} shape={saved.shape} />
```

### An avatar URL (Next.js route)

Serve avatars as cacheable image URLs, for places that only take a src.

```tsx
import { createAvatar } from '@illlustrations/avatars';
import { croods } from '@illlustrations/avatars/croods';

export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const svg = createAvatar(croods, { seed: id, size: 256, shape: 'circle' }).toString({ title: 'Avatar' });
  return new Response(svg, {
    headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' },
  });
}
```

Same seed and style version always give the same image, so these URLs cache well.

### Server-rendered HTML

Several inline SVG strings on one page need unique ID prefixes, or their masks clash.

```tsx
import { createAvatar } from '@illlustrations/avatars';
import { croods } from '@illlustrations/avatars/croods';

// idPrefix: starts with a letter; letters, digits, _ and - only; unique on the page.
// Clean the ID and add the index, so odd characters or repeated users never clash.
const prefix = (id: string, index: number) => `avatar-${index}-${id.replace(/[^A-Za-z0-9_-]/g, '')}`;

const html = users.map((user, index) =>
  createAvatar(croods, { seed: user.id, size: 40 })
    .toString({ idPrefix: prefix(user.id, index), title: user.name })
).join('');
```

<Avatar> and toDataUri() handle IDs for you. Only inline toString() output needs idPrefix. Seed with the raw ID; only the prefix needs cleaning.

### PNG for email and social images

Many email clients and social cards don't render SVG. Convert on the server.

```tsx
import sharp from 'sharp';
import { createAvatar } from '@illlustrations/avatars';
import { croods } from '@illlustrations/avatars/croods';

const svg = createAvatar(croods, { seed: 'alex', size: 512, background: '#EDEDFF' }).toString();
const png = await sharp(Buffer.from(svg)).png().toBuffer();
```

## Use with coding agents

Claude Code · this project:

```sh
mkdir -p .claude/skills/illlustrations-avatars && curl -o .claude/skills/illlustrations-avatars/SKILL.md https://illlustrations.co/docs/skill.md
```

Claude Code · every project:

```sh
mkdir -p ~/.claude/skills/illlustrations-avatars && curl -o ~/.claude/skills/illlustrations-avatars/SKILL.md https://illlustrations.co/docs/skill.md
```

Cursor:

```sh
mkdir -p .cursor/rules && curl -o .cursor/rules/illlustrations-avatars.mdc https://illlustrations.co/docs/skill.md
```

Codex and other agents:

```sh
curl -s https://illlustrations.co/docs/skill.md >> AGENTS.md
```

## License

Code: MIT. Croods artwork: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/); commercial use is
fine with credit:

```md
Croods v2 by [illlustrations / Vijay Verma](https://illlustrations.co),
© 2026, licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
```
