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