Facetcomponents

Avatar

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

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

import { Avatar, AvatarFallback, AvatarImage } from "../shared/ui/avatar";
<Avatar src={`rbxthumb://type=AvatarHeadShot&id=${userId}&w=150&h=150`}>
<AvatarImage />
<AvatarFallback Text="NR" />
</Avatar>
Fallbacks only — Loom has no Roblox content pipeline to fetch an rbxthumb:// from, so the image half cannot be previewed.

The parts

PartRendersClasses
AvatarFramesize-10 rounded-full bg-muted overflow-hidden
AvatarImageImageLabelsize-full rounded-full
AvatarFallbackTextLabelsize-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:

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

Progress is built the same way, for the same reason.

Every part rounds itself

Props

Avatar

PropTypeDescription
srcstringThe image source — an rbxassetid:// or rbxthumb:// URL. Read by the primitive, which decides whether the image or the fallback shows.
delayMsnumberHow long to hold the fallback back while the image loads. Stops a flash of initials on a fast load.
childrenReact.ReactNodeAvatarImage and AvatarFallback, in that order.
classNameClassNameThreaded 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. That is measured, not predicted.

A bigger avatar is an edit to the copied file:

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.