Components
ElvixAvatar

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.

AU
ElvixAvatar
editable wizard
AU
ElvixUserAvatar
read-only display

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
"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>
);
}
PropTypeRequiredDefaultDescription
applicationIdstringoptionalprovider appApp 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.
sizenumberoptional128Pixel diameter. Pinned across every pane (no expand-on-edit).
onChange(next: { sizes: number[]; updatedAt: string }) => voidoptionalnoneFires on successful upload or remove. updatedAt is your CDN cache-buster.
onResult(r: ElvixActionResult) => voidoptionalnoneTerminal 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
"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>
);
}
PropTypeRequiredDefaultDescription
sizenumberoptional40Pixel diameter (1x CSS px). srcset handles retina automatically.
shape"circle" | "square"optional"circle"Round chip or square tile.
appSlugstringoptionalproviderOverrides the app slug. Falls back to the <ElvixProvider> bootstrap.
userIdstringoptionalproviderRender 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.
namestringoptionalDisplay 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[] }optionalproviderPre-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 }optionalproviderOverride the user envelope when you already have it in scope.
fallbackSrcstringoptionalinitialsYour 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.
classNamestringoptional""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
"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>
);
}

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.

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.

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