# Avatar

A generative avatar: a mirrored dither face drawn from a name, with a group that overlaps and counts the rest.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. The dithercn agent skill, for building any dither on the engine, is at [/.well-known/agent-skills/dithercn/SKILL.md](/.well-known/agent-skills/dithercn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the dithercn engine. Install it when it is what you were asked for. For any other look, don't stretch its props to fit: write your own field on `DitherShader` ([Write your own dither](/docs/engine/write-your-own.md)), and read this example's source for technique. Its full source is in [https://dithercn.dev/r/avatar.json](https://dithercn.dev/r/avatar.json), under `files[].content`.

## Installation [#installation]

```bash
npx shadcn@latest add https://dithercn.dev/r/avatar.json
```

## Usage [#usage]

```tsx
import { Avatar, AvatarGroup } from "@/components/dithercn/avatar";

export function Owner() {
  return <Avatar name="ada@acme.dev" label="Ada Lovelace" status="online" />;
}

export function Viewers({ names }: { names: string[] }) {
  return (
    <AvatarGroup max={4}>
      {names.map((name) => (
        <Avatar key={name} name={name} />
      ))}
    </AvatarGroup>
  );
}
```

`name` seeds a small random generator that fills half of a 5 by 5 grid of blocks, and the other half mirrors it. A block is more likely to be on near the centre, so every face is a compact mark rather than noise, and the middle column always has at least two blocks. Each block is full or half tone, and the fill decides how half tone is drawn. Behind the face sits a disc of sparse dither in the same colour, densest at the top left as if lit, and kept one cell clear of the face so the mark stays crisp. The same string always gives the same face and colour, on the server and in the browser, so you can use an email or a user id and get a stable avatar for free.

Without `color`, the avatar picks one of six palette hues from the string. Pass a palette name or any CSS colour to set it yourself.

## Examples [#examples]

### Group with a count [#group-with-a-count]

`AvatarGroup` overlaps its avatars, cuts a gap around each in the surface colour, and replaces the rest with a "+N" chip. Set `outline` to the colour behind the group so the gaps disappear into it.

```tsx title="examples/avatar-group.tsx"
import { Avatar, AvatarGroup } from "@/components/dithercn/avatar";

const VIEWERS = [
  "Ada Lovelace",
  "Alan Turing",
  "Grace Hopper",
  "Edsger Dijkstra",
  "Barbara Liskov",
  "Ken Thompson",
  "Radia Perlman",
  "Donald Knuth",
  "Frances Allen",
] as const;

export default function AvatarGroupExample() {
  return (
    <div className="bg-background ring-border flex w-full max-w-sm flex-col gap-4 rounded-xl p-5 ring-1">
      <div className="flex items-center justify-between gap-4">
        <div className="flex flex-col gap-0.5">
          <span className="text-sm font-medium">Q3 launch plan</span>
          <span className="text-muted-foreground text-xs">
            Edited 4 minutes ago
          </span>
        </div>
        <AvatarGroup max={4} size={28}>
          {VIEWERS.map((name) => (
            <Avatar key={name} name={name} />
          ))}
        </AvatarGroup>
      </div>
      <div className="border-border flex items-center justify-between gap-4 border-t pt-4">
        <span className="text-muted-foreground text-xs">Shared with</span>
        <AvatarGroup max={6} size={22} outline="var(--background)">
          {VIEWERS.map((name) => (
            <Avatar key={name} name={name} fill="solid" />
          ))}
        </AvatarGroup>
      </div>
    </div>
  );
}
```

### Members with presence [#members-with-presence]

A member list seeded by email, labelled by name. `status` adds a dot and appends the state to the accessible name, as in "Alan Turing, busy". The owner gets a `ring`; the bot gets a grey hatched face.

```tsx title="examples/avatar-members.tsx"
import { Avatar } from "@/components/dithercn/avatar";
import type { AvatarStatus } from "@/components/dithercn/avatar";

const MEMBERS: readonly {
  name: string;
  email: string;
  role: string;
  status: AvatarStatus;
}[] = [
  {
    email: "ada@acme.dev",
    name: "Ada Lovelace",
    role: "Owner",
    status: "online",
  },
  {
    email: "alan@acme.dev",
    name: "Alan Turing",
    role: "Admin",
    status: "busy",
  },
  {
    email: "grace@acme.dev",
    name: "Grace Hopper",
    role: "Member",
    status: "away",
  },
  { email: "deploy-bot", name: "deploy-bot", role: "Bot", status: "offline" },
];

export default function AvatarMembers() {
  return (
    <ul className="bg-background ring-border divide-border flex w-full max-w-md flex-col divide-y rounded-xl ring-1">
      {MEMBERS.map((member, index) => (
        <li key={member.email} className="flex items-center gap-3 px-4 py-3">
          <Avatar
            name={member.email}
            label={member.name}
            size={36}
            status={member.status}
            fill={member.role === "Bot" ? "hatched" : "gradient"}
            color={member.role === "Bot" ? "grey" : undefined}
            ring={index === 0}
          />
          <div className="flex min-w-0 flex-1 flex-col">
            <span className="truncate text-sm font-medium">{member.name}</span>
            <span className="text-muted-foreground truncate font-mono text-xs">
              {member.email}
            </span>
          </div>
          <span className="text-muted-foreground text-xs">{member.role}</span>
        </li>
      ))}
    </ul>
  );
}
```

### Fills [#fills]

The same six names in every fill. Gradient draws half tone as a Bayer checker; dotted and hatched texture it instead; solid uses a lighter opacity. Full blocks are solid in every fill.

```tsx title="examples/avatar-fills.tsx"
import { Avatar } from "@/components/dithercn/avatar";
import { FILL_VARIANTS } from "@/lib/dither";

const NAMES = ["mira", "otto", "juno", "pax", "sol", "wren"] as const;

export default function AvatarFills() {
  return (
    // Avatars without a size read --avatar-size, so the row shrinks on phones.
    <div className="flex flex-col gap-4 [--avatar-size:30px] sm:[--avatar-size:44px]">
      {FILL_VARIANTS.map((fill) => (
        <div key={fill} className="flex items-center gap-2 sm:gap-3">
          <span className="text-muted-foreground w-14 shrink-0 font-mono text-xs">
            {fill}
          </span>
          {NAMES.map((name) => (
            <Avatar key={name} name={name} fill={fill} shape="square" />
          ))}
        </div>
      ))}
    </div>
  );
}
```

## Props [#props]

### Avatar [#avatar]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` (required) | `string` |  | The string the face and colour come from, such as a name, email or id. Also the accessible name. |
| `label` | `string` |  | Accessible name to use instead of name, when name is an id. |
| `size` | `number` | `40` | Diameter in pixels. Inside a group, the group's size. |
| `color` | `DitherColor` |  | A palette name or any CSS colour. Without it, one of six hues picked from name. |
| `fill` | `"gradient" \| "dotted" \| "hatched" \| "solid"` | `"gradient"` | How each block's tone is drawn. |
| `shape` | `"circle" \| "square"` | `"circle"` | A circle or a rounded square. |
| `ring` | `boolean` | `false` | A ring in the avatar's colour, for the current user or a selection. |
| `status` | `"online" \| "away" \| "busy" \| "offline"` |  | A presence dot in the corner, added to the accessible name. |
| `bloom` | `"off" \| "low" \| "high"` | `"off"` | A soft glow in the avatar's colour. |

### AvatarGroup [#avatargroup]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `max` | `number` |  | Show at most this many avatars, then a +N chip. |
| `size` | `number` | `32` | Diameter of every avatar in pixels. |
| `outline` | `string` | `"var(--background)"` | Colour of the gap around each avatar. Match the surface behind the group. |
| `moreLabel` | `(count: number) => string` | `"N more"` | What the +N chip announces to screen readers. |

`avatarFace(name)` and `avatarColor(name)` are exported too, if you want the same face elsewhere, such as an Open Graph image.

## Notes [#notes]

* The avatar is a `role="img"` element named by `label` or `name`, plus the status when there is one. The group is a `role="group"`; give it an `aria-label` such as "Viewers" when the context does not say.
* The face is one or a few SVG paths, cached per name and fill. No canvas, no client JavaScript.
* Avatars in a group lift by 2 pixels on hover on devices that can hover. With reduced motion they stay put.