# Avatar

> A circle drawn by the wrapper, because the primitive under it renders no instance at all.

Source: https://docs.astra-void.xyz/facet/components/avatar/

```bash
npx facet-rbxts add avatar
```

Copies `ui/avatar.tsx`, plus `lib/utils.ts`. Needs `@facet-ui/react-variants`,
`@lattice-ui/react-runtime@^0.8.0` and `@lattice-ui/react-avatar@^0.8.0`.

```tsx
import { Avatar, AvatarFallback, AvatarImage } from "../shared/ui/avatar";

<Avatar src={`rbxthumb://type=AvatarHeadShot&id=${userId}&w=150&h=150`}>
  <AvatarImage />
  <AvatarFallback Text="NR" />
</Avatar>
```

_Interactive preview: Fallbacks only — Loom has no Roblox content pipeline to fetch an rbxthumb:// from, so the image half cannot be previewed._

## The parts

| Part | Renders | Classes |
| --- | --- | --- |
| `Avatar` | `Frame` | `size-10 rounded-full bg-muted overflow-hidden` |
| `AvatarImage` | `ImageLabel` | `size-full rounded-full` |
| `AvatarFallback` | `TextLabel` | `size-full rounded-full bg-muted text-sm font-medium text-muted-foreground text-center` |

Renders a `Frame`. Unknown props forward onto it and are type-checked against it, so a prop `Frame` does not accept is a compile error.

That is `Avatar`. `AvatarImage` forwards onto an `ImageLabel` and `AvatarFallback` onto a `TextLabel`.

## The circle is drawn here, not by the primitive

`Avatar.Root` renders **no instance**. It tracks whether the image loaded and hands that answer to
`Image` and `Fallback`; it draws nothing. So unlike every other component in this tier, the visible
container is a plain `<frame>` in the copied file, and `Avatar.Root` wraps it.

That inverts the usual nesting:

```tsx
<AvatarPrimitive.Root delayMs={props.delayMs} src={props.src}>
  <frame className={cn(avatarVariants.root({ className: props.className }))} …>
    {props.children}
  </frame>
</AvatarPrimitive.Root>
```

[`Progress`](https://docs.astra-void.xyz/facet/components/progress.md) is built the same way, for the same reason.

## Every part rounds itself

> **Roblox clips to a rectangle**
>
> `rounded-full` appears three times — on the root, on the image and on the fallback — and it is not
> redundant. A `UICorner` rounds the instance it is on; it does not clip children to that shape.
> Round only the parent and a child that paints its own background pokes square corners out of the
> circle.
>
> `overflow-hidden` on the root is `ClipsDescendants`, which clips to the root's *rectangle* — not to
> its corner radius. It keeps a too-large image inside the box; it does not round it.

## Props

### `Avatar`

| Prop | Type | Description |
| --- | --- | --- |
| `src` | `string` | The image source — an rbxassetid:// or rbxthumb:// URL. Read by the primitive, which decides whether the image or the fallback shows. |
| `delayMs` | `number` | How long to hold the fallback back while the image loads. Stops a flash of initials on a fast load. |
| `children` | `React.ReactNode` | AvatarImage and AvatarFallback, in that order. |
| `className` | `ClassName` | Threaded into the root recipe's className slot inside the component. A class written at a Vela-compiled call site never reaches it — see Overriding from the call site. |

`AvatarFallback` takes `Text` and `className`; `AvatarImage` takes `className`.

## `size-10` is the only size there is

There is no `size` variant. `size-10` is stated on the root recipe, and a `className` at the call
site cannot change it — a class written there resolves to `Size` at that call site and the recipe
overwrites it, which is
[measured, not predicted](https://docs.astra-void.xyz/facet/guides/variants-and-classes.md#overriding-from-the-call-site).

A bigger avatar is an edit to the copied file:

```tsx
export const avatarVariants = {
  root: fv("size-14 rounded-full bg-muted overflow-hidden"),
  …
};
```

Or a variant, if a project needs two of them — which is what the file is for.
