# Avatar

An avatar drawn in glyphs: initials set over a lit disc of Braille dots, or a photo sampled into a glyph portrait, with a status glyph and overlapping groups.

> 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 asciicn agent skill, for building any ASCII effect on the engine, is at [/.well-known/agent-skills/asciicn/SKILL.md](/.well-known/agent-skills/asciicn/SKILL.md).

> **This is an example, not the only option.** It is one component built on the asciicn 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 `AsciiShader` ([Write your own](/docs/engine/write-your-own.md)), or draw a CPU grid with `AsciiGrid`, and read this example's source for technique. Its full source is in [https://asciicn.dev/r/avatar.json](https://asciicn.dev/r/avatar.json), under `files[].content`.

## Installation [#installation]

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

## Usage [#usage]

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

export function Owner() {
  return <Avatar name="Ada Lovelace" status="online" />;
}

export function Photo() {
  return <Avatar name="Buzz Aldrin" src="/team/buzz.jpg" size="lg" />;
}

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

Without `src`, the avatar takes up to two initials from `name` ("Ada Lovelace" is AL, "deploy-bot" DB, "[grace@navy.mil](mailto:grace@navy.mil)" G) and sets them in quadrant blocks (`▘▝▖▗▀▄▌▐█`), a pixel face whose solid 2×2 sub-cells stay legible even at 32px. They sit over a disc drawn in `charset`, Braille by default, whose mid tones are spread by ordered dithering. The disc is lit from the upper left, so it reads as a sphere, and it is tinted with a palette colour picked from `name`, so the same person always gets the same colour. A ring of blank samples around the letters keeps them clear of the disc.

With `src`, the photo is cover-fitted, area-averaged into the glyph grid, stretched to its full tonal range and drawn in any charset. In a light theme the tones flip, so dark parts of the photo get the dense glyphs. `colorMode="truecolor"` keeps the photo's colour per glyph.

The grid is always twice as many columns as rows, which makes it square. `sm` is 4 rows at 32px, `md` 6 rows at 48px, `lg` 8 rows at 72px and `xl` 12 rows at 112px. Pass a number for any size in pixels.

## Examples [#examples]

### Photo portraits [#photo-portraits]

The same photo at 160px in Braille, Dense 70, ANSI blocks, and Dense 70 in truecolor.

```tsx title="examples/avatar-portraits.tsx"
import { Avatar } from "@/components/asciicn/avatar";
import type { AvatarProps } from "@/components/asciicn/avatar";

const LOOKS: readonly { caption: string; props: Partial<AvatarProps> }[] = [
  { caption: "braille", props: { charset: "braille" } },
  { caption: "dense", props: { charset: "dense" } },
  { caption: "blocks", props: { charset: "blocks" } },
  {
    caption: "truecolor",
    props: { charset: "dense", colorMode: "truecolor" },
  },
];

export default function AvatarPortraits() {
  return (
    <ul className="grid w-full max-w-md grid-cols-2 justify-items-center gap-8">
      {LOOKS.map((look) => (
        <li key={look.caption} className="flex flex-col items-center gap-3">
          <Avatar
            name="Buzz Aldrin"
            src="/examples/aldrin.jpg"
            size={160}
            {...look.props}
          />
          <span className="text-muted-foreground font-mono text-xs">
            {look.caption}
          </span>
        </li>
      ))}
    </ul>
  );
}
```

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

`AvatarGroup` overlaps each avatar by a quarter of its width, with a ring in `outline` between them. Past `max`, the rest collapse into a grey `+n` disc.

```tsx title="examples/avatar-group.tsx"
import { Avatar, AvatarGroup } from "@/components/asciicn/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 flex w-full max-w-sm flex-col gap-4 rounded-lg border p-5">
      <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="sm" aria-label="9 viewers">
          {VIEWERS.map((name) => (
            <Avatar key={name} name={name} />
          ))}
        </AvatarGroup>
      </div>
      <div className="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={28}
          outline="var(--background)"
          aria-label="9 people"
        >
          {VIEWERS.map((name) => (
            <Avatar key={name} name={name} charset="quadrants" />
          ))}
        </AvatarGroup>
      </div>
    </div>
  );
}
```

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

`status` sets a glyph in the corner. Each status has its own shape as well as its own colour: `●` online, `◐` away, `■` busy, `○` offline. The bot uses `shape="square"` and a grey tint.

```tsx title="examples/avatar-members.tsx"
import { Avatar } from "@/components/asciicn/avatar";
import type { AvatarStatus } from "@/components/asciicn/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 flex w-full max-w-md flex-col divide-y rounded-lg border">
      {MEMBERS.map((member) => (
        <li key={member.email} className="flex items-center gap-3 px-4 py-3">
          <Avatar
            name={member.name}
            size={40}
            status={member.status}
            shape={member.role === "Bot" ? "square" : "circle"}
            color={member.role === "Bot" ? "grey" : undefined}
          />
          <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>
  );
}
```

## Props [#props]

### Avatar [#avatar]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` (required) | `string` |  | Who it is. Gives the initials, the default tint and the accessible name. |
| `label` | `string` | `name` | Accessible name, when it should differ from name. |
| `src` | `string` |  | A photo to draw as a glyph portrait instead of initials. |
| `size` | `"sm" \| "md" \| "lg" \| "xl" \| number` | `"md"` | 32, 48, 72 or 112px, or any size in pixels. |
| `charset` | `CharsetName` | `"braille"` | Glyphs to draw with. |
| `chars` | `string` |  | A custom ramp from empty to dense. Wins over charset. |
| `colorMode` | `"mono" \| "truecolor"` | `"mono"` | With src: truecolor keeps the photo's colour per glyph. |
| `color` | `AsciiColor` | `Picked from name` | Tint of the disc or photo, a palette name or any CSS colour. |
| `shape` | `"circle" \| "square"` | `"circle"` | A disc, or a square that fills the whole grid. |
| `status` | `"online" \| "away" \| "busy" \| "offline"` |  | A status glyph in the corner. |

### AvatarGroup [#avatargroup]

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` |  | The avatars, in order. |
| `max` | `number` | `All` | Show at most this many, then a +n count. |
| `size` | `"sm" \| "md" \| "lg" \| "xl" \| number` | `"md"` | Size of every avatar in the group. |
| `outline` | `string` | `"var(--ascii-paper)"` | Colour of the ring between overlapping avatars. |

## Notes [#notes]

* Each avatar is `role="img"` named by `label` or `name`, plus its status ("Ada Lovelace, online"). The glyphs are hidden from screen readers. Give the group an `aria-label` such as "9 viewers"; the count reads "and 5 more".
* The disc is computed without a canvas, so it renders on the server with the initials as plain text over it. After mount, the initials are redrawn in glyphs; the box keeps its size, so nothing shifts.
* Photos load with `crossOrigin="anonymous"`, so images from another origin need CORS headers to be sampled. Until a photo loads, the avatar shows the initials disc.
* Every avatar is a few hundred glyphs of DOM text. The grid is sampled once per change of name, photo, charset or theme, never per frame.