# Banner

> 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.

> **Note:** Same srcset hero, same gradient empty-state, same CDN. The only difference is whether the host wants the wizard mounted. Pick the editable one on the profile editor. Pick the read-only one on public profile hero, account dashboard, anywhere display-only.

## Editable: ElvixBanner

In-frame replace + crop + remove wizard. The frame keeps its 3:1 aspect across every pane so it never pushes siblings around mid-edit. Mount inside an `<ElvixProvider>`.

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

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

export function ProfileBannerEditor({ applicationId }: { applicationId: string }) {
  return (
    <ElvixProvider clientId={applicationId}>
      <ElvixBanner
        applicationId={applicationId}
        cornerRadius={14}
        onChange={(next) => {
          // Re-key downstream <ElvixUserBanner> renders with next.updatedAt
          // to bust the CDN cache without a full page reload.
          console.log("banner updated", next.sizes, next.updatedAt);
        }}
      />
    </ElvixProvider>
  );
}
```

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `applicationId` | `string` | optional | provider app | App scope. Accepts EITHER your public clientId OR the internal Application.id (both resolve server-side). Omit it to inherit the app from `<ElvixProvider>`. |
| `cornerRadius` | `number` | optional | `14` | Border-radius in px. Matches the SDK card chrome by default. |
| `onChange` | `(next: { sizes: number[]; updatedAt: Date \| number }) => void` | optional | none | Fires on successful upload or remove. `updatedAt` is your CDN cache-buster. |
| `onResult` | `(r: ElvixBannerResult) => void` | optional | none | Terminal callback for every attempt (success and failure). Safe metadata only, never image bytes. |

## Read-only: ElvixUserBanner

Static 3:1 hero with srcset. Fills its parent width by default; pass `containerPx` to tell the browser which variant to pick. No edit affordance, no callbacks.

<!-- components/public-profile-hero.tsx -->
```tsx
"use client";

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

// Public profile hero. Pass userId only when rendering someone else.
export function PublicProfileHero({ userId }: { userId?: string }) {
  return (
    <ElvixProvider clientId="your-app">
      <ElvixUserBanner userId={userId} containerPx={960} />
    </ElvixProvider>
  );
}
```

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `userId` | `string` | optional | provider | Render ANY user by id. Fetches that user's CENTRALIZED banner from `/public/api/users/<id>/media-meta` with NO session and NO membership prop. Omit to render the signed-in user from `<ElvixProvider>` context. |
| `appSlug` | `string` | optional | provider | Override the CDN app slug. Rarely needed — by-id mode reads it from the media-meta response; current-user mode from the bootstrap. |
| `membership` | `{ bannerUpdatedAt: Date \| number; bannerSizes: number[] }` | optional | provider | Override the membership envelope. `bannerUpdatedAt` is the CDN cache-buster, `bannerSizes` drives the srcset. |
| `containerPx` | `number` | optional | parent width | Container max-width in CSS px. Drives the `sizes` hint on srcset so the browser picks the smallest crisp variant. |
| `className` | `string` | optional | none | Passes through to the root element. |
| `emptyClassName` | `string` | optional | gradient | Background class for the empty-state placeholder when no banner has been uploaded. |
| `fallbackSrc` | `string` | optional | gradient | Your own placeholder IMAGE for the no-banner state, shown instead of the default gradient. A URL on your origin or a `data:` URI. Ignored once the user sets a real banner. |

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

Like `ElvixUserAvatar`, passing `userId` renders a THIRD party's banner from their elvix id alone — NO session, NO membership envelope. The banner is whatever that user set once in their elvix account; it appears the same on every elvix-powered app. Origin-gated to your app's registered origins, cached + de-duplicated per user id.

> **Note:** **Live updates, no wiring (0.9.4+).** When `ElvixBanner` uploads or removes, every `ElvixUserBanner` for the same user refreshes live — same tab, across tabs, and across apps + devices. Same-origin tabs sync instantly via `BroadcastChannel`; cross-app / cross-device changes arrive over the shared `/api/media/stream` SSE within a few seconds. No websocket, no manual re-key, no reload. `onChange.updatedAt` remains the CDN cache-buster for server-rendered surfaces that mount fresh. `ElvixUserBanner` also takes `cornerRadius` so its rounding matches `ElvixBanner` (both default to 14px).

> **Warning:** The editable frame captures pointer events across every pane until the user confirms or backs out. It will not push siblings around, but adjacent click targets are blocked inside the frame area.


## Related

- [Avatar](/docs/components/elvix-avatar.md): 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.
- [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.
