Latticecomponents

Avatar

An image-with-fallback primitive that tracks load status, debounces the fallback with a delay, and lets your component own the visuals.

@lattice-ui/react-avatarStable directionimport Avatardepends on runtime

Avatar represents a player or entity with an image, falling back to placeholder content when there is no source or the image has not loaded. It tracks the load lifecycle of the underlying ImageLabel and coordinates when the image versus the fallback is shown, so you never flash a placeholder during a fast load or leave an empty box on a broken asset.

Reach for Avatar wherever you show a thumbnail that might be missing or slow — headshots, group icons, item images — and want the image-then-fallback handoff handled for you, including a short delay before the fallback appears so quick loads stay seamless.

Preview

The component running live in the browser — the same @rbxts/react tree Roblox renders, fully interactive.

Edit

Import

import { Avatar } from "@lattice-ui/react-avatar";

Anatomy

Compose Root around an Image and a Fallback. The root owns the source and status; the image shows once loaded and the fallback shows otherwise. The root itself renders no instance — it is a pure context provider — so both parts mount directly into whatever parent you place the avatar in.

Avatar anatomy

Avatar anatomy
<Avatar.Root src="...">
<Avatar.Image />
<Avatar.Fallback />
</Avatar.Root>
PartRequiredResponsibility
Avatar.RootyesOwns the source, load status, and the fallback delay; shares them through context.
Avatar.ImageyesRenders the image and reports its load status back to the root.
Avatar.FallbacknoPlaceholder shown while the image is missing, loading past the delay, or errored.

Examples

Basic image with initials fallback

The smallest useful avatar: a source on the root, the default 40x40 circular image, and an initials fallback. The fallback uses asChild to render your own label — the default fallback is a placeholder textlabel with "AB" text, so real content should replace it rather than sit inside it.

BasicAvatar.tsx
import { Avatar } from "@lattice-ui/react-avatar";
export function BasicAvatar() {
return (
<Avatar.Root src="rbxassetid://1234567890">
<Avatar.Image />
<Avatar.Fallback asChild>
<textlabel
BackgroundColor3={Color3.fromRGB(65, 72, 89)}
BorderSizePixel={0}
Size={UDim2.fromOffset(40, 40)}
Text="MJ"
TextColor3={Color3.fromRGB(235, 240, 248)}
TextSize={14}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</textlabel>
</Avatar.Fallback>
</Avatar.Root>
);
}

Tuning the fallback delay

delayMs controls how long a loading avatar stays blank before the fallback appears. The default is 250, which absorbs most fast loads without a placeholder flash; raise it when your art direction prefers a longer blank window, or set it to 0 to show the fallback the moment loading starts. The delay only applies while loading — an empty or absent src skips it and shows the fallback immediately, and an errored load always shows it.

LoadoutSlotAvatar.tsx
import { Avatar } from "@lattice-ui/react-avatar";
export function LoadoutSlotAvatar(props: { iconSrc?: string }) {
return (
<Avatar.Root src={props.iconSrc} delayMs={400}>
<Avatar.Image />
<Avatar.Fallback asChild>
<textlabel
BackgroundColor3={Color3.fromRGB(65, 72, 89)}
BorderSizePixel={0}
Size={UDim2.fromOffset(40, 40)}
Text="?"
TextColor3={Color3.fromRGB(235, 240, 248)}
TextSize={16}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</textlabel>
</Avatar.Fallback>
</Avatar.Root>
);
}

Player headshot from a UserId

The most common source in a Roblox game: a rbxthumb:// headshot URL built from a UserId. The engine resolves and caches the thumbnail itself, Avatar.Image reports loaded once the ImageLabel finishes, and the initials cover the gap for players whose thumbnails are slow or unavailable.

PlayerHeadshot.tsx
import { Avatar } from "@lattice-ui/react-avatar";
export function PlayerHeadshot(props: { userId: number; displayName: string }) {
const src = `rbxthumb://type=AvatarHeadShot&id=${props.userId}&w=150&h=150`;
const initials = props.displayName.sub(1, 2).upper();
return (
<Avatar.Root src={src}>
<Avatar.Image />
<Avatar.Fallback asChild>
<textlabel
BackgroundColor3={Color3.fromRGB(65, 72, 89)}
BorderSizePixel={0}
Size={UDim2.fromOffset(40, 40)}
Text={initials}
TextColor3={Color3.fromRGB(235, 240, 248)}
TextSize={14}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</textlabel>
</Avatar.Fallback>
</Avatar.Root>
);
}

Status ring composition

Presence indicators are app-owned visuals: the primitive handles only the image-versus-fallback handoff, so a status ring is just your own frame composed around it. Because Avatar.Root renders no instance, both parts mount straight into the ring frame; asChild on the image lets you center it inside the slightly larger wrapper.

StatusAvatar.tsx
import { Avatar } from "@lattice-ui/react-avatar";
export function StatusAvatar(props: { userId: number; online: boolean }) {
const src = `rbxthumb://type=AvatarHeadShot&id=${props.userId}&w=150&h=150`;
const statusColor = props.online ? Color3.fromRGB(64, 200, 120) : Color3.fromRGB(96, 104, 122);
return (
<frame BackgroundTransparency={1} Size={UDim2.fromOffset(48, 48)}>
<uicorner CornerRadius={new UDim(1, 0)} />
<uistroke Color={statusColor} Thickness={2} />
<Avatar.Root src={src}>
<Avatar.Image asChild>
<imagelabel
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundTransparency={1}
BorderSizePixel={0}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(40, 40)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</imagelabel>
</Avatar.Image>
<Avatar.Fallback asChild>
<frame
AnchorPoint={new Vector2(0.5, 0.5)}
BackgroundColor3={Color3.fromRGB(65, 72, 89)}
BorderSizePixel={0}
Position={UDim2.fromScale(0.5, 0.5)}
Size={UDim2.fromOffset(40, 40)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</frame>
</Avatar.Fallback>
</Avatar.Root>
<frame
AnchorPoint={new Vector2(1, 1)}
BackgroundColor3={statusColor}
BorderSizePixel={0}
Position={UDim2.fromScale(1, 1)}
Size={UDim2.fromOffset(12, 12)}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<uistroke Color={Color3.fromRGB(24, 26, 32)} Thickness={2} />
</frame>
</frame>
);
}

Avatar stack

A party-members row with overlapping avatars. Negative uilistlayout padding pulls each wrapper over the previous one, descending ZIndex keeps the leftmost member on top, and a stroke in the panel’s background color creates the separation cut. Each member gets their own Avatar.Root, so slow thumbnails resolve independently.

PartyStack.tsx
import { Avatar } from "@lattice-ui/react-avatar";
const PARTY = [
{ userId: 156, initials: "BH" },
{ userId: 261, initials: "SH" },
{ userId: 1179762, initials: "JN" },
];
export function PartyStack() {
return (
<frame BackgroundTransparency={1} Size={UDim2.fromOffset(160, 40)}>
<uilistlayout
FillDirection={Enum.FillDirection.Horizontal}
Padding={new UDim(0, -12)}
SortOrder={Enum.SortOrder.LayoutOrder}
/>
{PARTY.map((member, index) => (
<frame
key={member.userId}
BackgroundColor3={Color3.fromRGB(24, 26, 32)}
BorderSizePixel={0}
LayoutOrder={index}
Size={UDim2.fromOffset(40, 40)}
ZIndex={PARTY.size() - index}
>
<uicorner CornerRadius={new UDim(1, 0)} />
<uistroke Color={Color3.fromRGB(24, 26, 32)} Thickness={2} />
<Avatar.Root src={`rbxthumb://type=AvatarHeadShot&id=${member.userId}&w=150&h=150`}>
<Avatar.Image />
<Avatar.Fallback asChild>
<textlabel
BackgroundColor3={Color3.fromRGB(65, 72, 89)}
BorderSizePixel={0}
Size={UDim2.fromOffset(40, 40)}
Text={member.initials}
TextColor3={Color3.fromRGB(235, 240, 248)}
TextSize={13}
>
<uicorner CornerRadius={new UDim(1, 0)} />
</textlabel>
</Avatar.Fallback>
</Avatar.Root>
</frame>
))}
</frame>
);
}

How it behaves

Load status

Avatar.Root tracks an AvatarStatus of "idle", "loading", "loaded", or "error". On mount and whenever src changes, it enters "loading" if a non-empty source is set, or "error" if the source is empty or absent — the "idle" member exists in the union but the built-in parts never produce it. Changing src restarts the cycle: status resets to "loading" and the fallback delay timer starts over, with a sequence guard so a stale timer from a previous source cannot fire.

Avatar.Image reports the actual load result. It checks its ImageLabel’s IsLoaded property immediately and subscribes to GetPropertyChangedSignal("IsLoaded"), setting the shared status to "loaded" once the engine finishes the asset; if its resolved source is empty it reports "error" instead. The image is only Visible while the status is "loaded", so a broken or in-flight asset never shows as an empty box.

Source resolution

Avatar.Image resolves its source from its own src prop first, then falls back to the root’s src. Set the source once on the root for the common case, or override it per image when one avatar composition needs a different asset than the shared context.

Fallback timing

Avatar.Fallback derives visibility from the status and the delay: hidden once "loaded", always shown on "error", and otherwise shown only after the delay has elapsed. The root starts a delayMs timer (default 250, clamped to a minimum of 0) when a source begins loading; until it fires, the fallback stays hidden so a fast load never flashes a placeholder. When there is no source, the delay is treated as elapsed immediately and the fallback appears at once.

This rule is exported as resolveAvatarFallbackVisible(status, delayElapsed) alongside the AvatarStatus type, so custom status-driven parts can share the exact same visibility logic.

Default parts and asChild

Avatar.Root renders no instance of its own — it only provides context — so Image and Fallback mount directly into the surrounding parent and you control layout entirely from outside. The default Avatar.Image is a 40x40 circular imagelabel with a transparent background; the default Avatar.Fallback is a 40x40 circular textlabel with placeholder "AB" text, and children passed without asChild render inside that label rather than replacing it.

With asChild, Avatar.Image merges the resolved Image source, load-bound Visible, and its status-tracking ref onto your single child element, and Avatar.Fallback merges only the derived Visible. Both parts error if asChild is set without a child.

API reference

Avatar.Root

PropTypeDescription
srcstringDefault image source shared with Avatar.Image through context. An empty or absent source resolves to the error status and shows the fallback immediately.
delayMsnumberMilliseconds to wait before showing the fallback while loading, so fast loads do not flash a placeholder. Defaults to 250 and is clamped to a minimum of 0.
childrenReact.ReactNodeThe image and fallback parts. The root renders no instance of its own, so children mount into the surrounding parent.

Avatar.Image

PropTypeDescription
srcstringImage source for this part. Overrides the root's src when set; otherwise the root's src is used.
asChildbooleanMerge the resolved source, load-bound visibility, and status-tracking ref onto the single child element instead of rendering the default 40x40 circular imagelabel. The child must be an imagelabel.
childrenReact.ReactElementThe element to render. Required when asChild is set.

Avatar.Fallback

PropTypeDescription
asChildbooleanMerge the derived visibility onto the single child element instead of rendering the default 40x40 circular textlabel.
childrenReact.ReactElementThe placeholder element to render, such as initials or an icon. Required when asChild is set; otherwise rendered inside the default label on top of its placeholder text.