# Avatar

> Centralized avatars, rendered by user id. `ElvixUserAvatar` shows ANY user’s photo from their id alone (no session, no membership) with a custom/OAuth/initials fallback and live cross-app updates. `ElvixAvatar` is the editable wizard that writes the user’s one global photo.

> **Note:** Same image, same custom / Google / initials fallback chain, same CDN. The only difference is whether the host wants the wizard mounted. Pick the editable one on profile and settings surfaces. Pick the read-only one everywhere else.

## Editable: ElvixAvatar

In-place wizard: display, choice (replace or remove), crop via react-easy-crop, remove confirm, working spinner. Every pane stays inside the circle at the same `size`, so reserve that square in your layout. Mount inside an `<ElvixProvider>`.

<!-- app/profile/avatar.tsx -->
```tsx
"use client";

import { ElvixAvatar, ElvixProvider } from "@elvix.is/sdk/react";

export function ProfileAvatar({ applicationId }: { applicationId: string }) {
  return (
    <ElvixProvider clientId={applicationId}>
      <ElvixAvatar
        applicationId={applicationId}
        size={128}
        onChange={(next) => {
          // Re-key downstream <ElvixUserAvatar> renders with next.updatedAt
          // to bust the CDN cache without a full page reload.
          console.log("avatar updated", next.sizes, next.updatedAt);
        }}
      />
    </ElvixProvider>
  );
}
```

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `applicationId` | `string` | optional | provider app | App scope for the change event/context. Accepts EITHER your public clientId OR the internal Application.id. NOTE: the photo itself is CENTRALIZED (0.10.0+) — editing or removing it here changes the user's ONE elvix photo everywhere, not a per-app copy. Omit it to inherit the app from `<ElvixProvider>`. |
| `mode` | `"edit" \| "view"` | optional | `"edit"` | `edit` mounts the in-place wizard. `view` renders the read-only, live-updating display (delegates to `ElvixUserAvatar`) — one component for both surfaces. |
| `size` | `number` | optional | `128` | Pixel diameter. Pinned across every pane (no expand-on-edit). |
| `onChange` | `(next: { sizes: number[]; updatedAt: string }) => void` | optional | none | Fires on successful upload or remove. `updatedAt` is your CDN cache-buster. |
| `onResult` | `(r: ElvixActionResult) => void` | optional | none | Terminal callback for every attempt (success + failure). Safe metadata only. |

## Read-only: ElvixUserAvatar

Static avatar with retina via srcset. No edit affordance, no click target, no callbacks. Default 40px (the navigation-chip size); override `size` for comment lists, profile headers, or larger surfaces.

<!-- components/nav-user-chip.tsx -->
```tsx
"use client";

import { ElvixProvider, ElvixUserAvatar } from "@elvix.is/sdk/react";

// Three sizes, three places. Same component, no wizard, no callbacks.
export function NavUserChip({ name }: { name: string }) {
  return (
    <ElvixProvider clientId="your-app">
      <div className="flex items-center gap-2">
        <ElvixUserAvatar size={32} />
        <span>{name}</span>
      </div>
    </ElvixProvider>
  );
}
```

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `size` | `number` | optional | `40` | Pixel diameter (1x CSS px). srcset handles retina automatically. |
| `shape` | `"circle" \| "square"` | optional | `"circle"` | Round chip or square tile. |
| `appSlug` | `string` | optional | provider | Overrides the app slug. Falls back to the `<ElvixProvider>` bootstrap. |
| `userId` | `string` | optional | provider | Render ANY user by id. Fetches that user's CENTRALIZED avatar from `/public/api/users/<id>/media-meta` with NO session and NO membership prop — the photo they set once in their elvix account, visible everywhere. Omit to render the signed-in user from `<ElvixProvider>` context. |
| `name` | `string` | optional | — | Display name for the initials fallback + `aria-label`. Pass this in by-id mode: the endpoint returns photo meta only, never identity text, so the host supplies the name it already holds. |
| `membership` | `{ avatarUpdatedAt: Date \| number; avatarSizes: number[] }` | optional | provider | Pre-resolved membership envelope. When supplied, NO by-id fetch happens (the host already holds the meta). `avatarUpdatedAt` is the CDN cache-buster. |
| `user` | `{ name?: string \| null; email?: string \| null; avatarUrl?: string \| null }` | optional | provider | Override the user envelope when you already have it in scope. |
| `fallbackSrc` | `string` | optional | initials | Your own placeholder IMAGE for the no-photo state, shown instead of the initials chip. A URL on your origin or a `data:` URI. Ignored once a custom or OAuth photo resolves. |
| `className` | `string` | optional | `""` | Passes through to the rendered element. |

## Any user, by id — centralized (0.10.0+)

A user's photo is CENTRALIZED in elvix: set once in any elvix-powered app (or the account dashboard), it appears everywhere. Pass `userId` + `name` and `ElvixUserAvatar` fetches that user's account photo from the public `/public/api/users/<id>/media-meta` endpoint — NO session, NO membership envelope, NO per-app lookup. This is how you render a third party you only know by elvix id: a skill owner, a comment author, a teammate in a list. The endpoint is origin-gated (your app's registered origins) and returns photo meta only — never name or email — so you pass the `name` you already hold for the initials fallback.

<!-- components/skill-owner.tsx -->
```tsx
"use client";

import { ElvixProvider, ElvixUserAvatar } from "@elvix.is/sdk/react";

// Render a THIRD party — a skill owner, a comment author — knowing only
// their elvix user id. No session, no membership prop, no per-app data.
// The photo is whatever they set once in their elvix account.
export function SkillOwner({ ownerId, ownerName }: { ownerId: string; ownerName: string }) {
  return (
    <ElvixProvider clientId="your-app">
      <ElvixUserAvatar userId={ownerId} name={ownerName} size={40} />
    </ElvixProvider>
  );
}
```

> **Note:** Fetches are cached + de-duplicated per user id, so a grid of 50 rows for the same author fires ONE request. The image URL carries a `?v=<updatedAt>` cache-buster, so a photo change surfaces to every consumer within the endpoint's short cache window.

> **Note:** **Live updates, no wiring (0.9.4+).** When `ElvixAvatar` uploads or removes an image, every `ElvixUserAvatar` (and `ElvixAvatar mode="view"`) for the same user updates live — same tab, across tabs, AND across apps + devices. Same-origin tabs sync instantly via an internal client store + `BroadcastChannel`; cross-app / cross-device changes arrive over a shared Server-Sent Events stream (`/api/media/stream`) within a few seconds. No websocket, no manual re-key, no reload. `onChange.updatedAt` is still your CDN cache-buster for server-rendered surfaces that mount fresh.

> **Warning:** The editable widget never resizes between panes. Inside a flex row the cropping pane will not push siblings around, but it captures pointer events inside the circle until the user confirms or backs out.


## Related

- [Banner](/docs/components/elvix-banner.md): Centralized banners, rendered by user id. `ElvixUserBanner` shows ANY user’s 3:1 hero from their id alone (no session). `ElvixBanner` is the editable in-frame wizard that writes the user’s one global banner.
- [ElvixIdentityForm](/docs/components/elvix-identity-form.md): Hosted profile editor: name, locale, timezone, pronouns, contact. Plugs straight into the elvix User record. In-frame edit → confirm → done lifecycle.
