@lattice-ui/react-avatarStable directionimport Avatardepends on runtimeAvatar shows a player or entity image, falling back to placeholder content when there is no source
or the image has not loaded. It tracks the underlying ImageLabel’s load lifecycle, so a fast load
never flashes a placeholder and a broken asset never leaves an empty box.
Preview
import { Avatar } from "@lattice-ui/react-avatar";import { React } from "@lattice-ui/react-runtime";import { Text, useTheme } from "@lattice-ui/react-style";
type Member = { initials: string; name: string; role: string; accent?: boolean;};
function AvatarExample() { const { theme } = useTheme();
const members: Array<Member> = [ { initials: "AC", name: "Ava Chen", role: "Design lead", accent: true }, { initials: "NP", name: "Noah Park", role: "Engineer" }, { initials: "MT", name: "Mia Torres", role: "Product" }, ];
return ( <frame BackgroundColor3={theme.colors.surfaceElevated} BorderSizePixel={0} Size={UDim2.fromScale(1, 1)}> <uicorner CornerRadius={new UDim(0, theme.radius.lg)} /> <uistroke Color={theme.colors.border} Thickness={1} /> <uipadding PaddingBottom={new UDim(0, theme.space[16])} PaddingLeft={new UDim(0, theme.space[20])} PaddingRight={new UDim(0, theme.space[20])} PaddingTop={new UDim(0, theme.space[16])} /> <uilistlayout FillDirection={Enum.FillDirection.Vertical} Padding={new UDim(0, theme.space[10])} />
<frame BackgroundTransparency={1} LayoutOrder={0} Size={UDim2.fromOffset(260, 40)}> <Text BackgroundTransparency={1} Font={Enum.Font.GothamBold} Size={UDim2.fromOffset(260, 18)} Text="Project members" TextColor3={theme.colors.textPrimary} TextSize={theme.typography.bodyMd.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> <Text BackgroundTransparency={1} Position={UDim2.fromOffset(0, 22)} Size={UDim2.fromOffset(260, 16)} Text="3 people · 2 online" TextColor3={theme.colors.textSecondary} TextSize={theme.typography.labelSm.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> </frame>
{members.map((member, index) => ( <frame BackgroundTransparency={1} key={member.initials} LayoutOrder={index + 1} Size={UDim2.fromOffset(260, 40)} > <Avatar.Root> <frame BackgroundColor3={member.accent ? theme.colors.accent : theme.colors.surface} BorderSizePixel={0} Position={UDim2.fromOffset(0, 2)} Size={UDim2.fromOffset(36, 36)} > <uicorner CornerRadius={new UDim(1, 0)} /> <uistroke Color={member.accent ? theme.colors.accent : theme.colors.border} Thickness={1} /> <Avatar.Fallback asChild> <textlabel BackgroundTransparency={1} BorderSizePixel={0} Font={Enum.Font.GothamMedium} Size={UDim2.fromScale(1, 1)} Text={member.initials} TextColor3={member.accent ? theme.colors.accentContrast : theme.colors.textPrimary} TextSize={theme.typography.labelSm.textSize} /> </Avatar.Fallback> </frame> </Avatar.Root> <Text BackgroundTransparency={1} Font={Enum.Font.GothamMedium} Position={UDim2.fromOffset(48, 2)} Size={UDim2.fromOffset(212, 18)} Text={member.name} TextColor3={theme.colors.textPrimary} TextSize={theme.typography.labelSm.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> <Text BackgroundTransparency={1} Position={UDim2.fromOffset(48, 22)} Size={UDim2.fromOffset(212, 16)} Text={member.role} TextColor3={theme.colors.textSecondary} TextSize={theme.typography.labelSm.textSize} TextXAlignment={Enum.TextXAlignment.Left} /> </frame> ))} </frame> );}Import
import { Avatar } from "@lattice-ui/react-avatar";Anatomy
The root owns the source and status. The image shows once loaded, and the fallback otherwise. Root
renders no instance, so both parts mount directly into whatever parent you place the avatar in.
Avatar anatomy
<Avatar.Root src="..."> <Avatar.Image /> <Avatar.Fallback /></Avatar.Root>| Part | Required | Responsibility |
|---|---|---|
Avatar.Root | yes | Owns the source, load status, and the fallback delay; shares them through context. |
Avatar.Image | yes | Renders the image and reports its load status back to the root. |
Avatar.Fallback | no | Placeholder shown while the image is missing, loading past the delay, or errored. |
Examples
Basic image with initials fallback
A source on the root, the default circular image, and an initials fallback. The fallback uses
asChild to render your own label, since the default is a placeholder textlabel that real content
should replace rather than sit inside.
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
250 absorbs most fast loads without a flash. 0 shows the fallback the moment loading starts. The
delay only applies while loading — an empty src or an errored load shows the fallback immediately.
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 is a rbxthumb:// headshot URL built from a UserId. The
engine resolves and caches it, Avatar.Image reports loaded once the ImageLabel finishes, and
the initials cover the gap for slow or unavailable thumbnails.
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
The primitive handles only the image-versus-fallback handoff, so a status ring is your own frame
composed around it. Because Root renders no instance, both parts mount straight into the ring
frame.
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
Negative uilistlayout padding pulls each wrapper over the previous one. Descending ZIndex keeps
the leftmost on top, and a stroke in the panel’s background colour cuts them apart. Each member gets
their own Avatar.Root, so slow thumbnails resolve independently.
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
Root tracks an AvatarStatus of "idle", "loading", "loaded" or "error". On mount, and
whenever src changes, it enters "loading" for a non-empty source or "error" otherwise.
"idle" exists in the union, but the built-in parts never produce it. Changing src restarts the
cycle, with a sequence guard so a stale timer cannot fire.
Avatar.Image checks IsLoaded immediately and subscribes to its changed signal. It sets the
status to "loaded" once the engine finishes, or "error" when its resolved source is empty. It is
only Visible while "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 first, then the root’s — so set it once on the
root, or override per image.
Fallback timing
Avatar.Fallback is hidden once "loaded", always shown on "error", and otherwise shown only
after the delay elapses. The root starts a delayMs timer when a source begins loading, so a fast
load never flashes a placeholder. With no source the delay counts as elapsed immediately.
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
Root renders no instance, so Image and Fallback mount into the surrounding parent and you
control layout from outside. The defaults are a 40x40 circular imagelabel and a matching
textlabel with placeholder "AB" text. 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.
Keep Image and Fallback mounted as siblings. Each derives its own visibility from the shared status, so do not conditionally mount one or the other yourself.
Avatar.Image narrows its ref with IsA("ImageLabel") before watching IsLoaded. Any other class never attaches load tracking, an imagebutton included. The status never reaches "loaded", and the image stays invisible.
Without asChild, Avatar.Fallback renders a textlabel reading "AB" and puts your children inside it, so custom content sits on top of the placeholder. Pass your element with asChild to replace the label instead.
Status comes from ImageLabel.IsLoaded, so it reflects the engine’s own asset pipeline. Pass a resolved asset string as src. An empty string counts as an error and shows the fallback immediately.
API reference
Avatar.Root
| Prop | Type | Description |
|---|---|---|
| src | string | Default image source shared with Avatar.Image through context. An empty or absent source resolves to the error status and shows the fallback immediately. |
| delayMs | number | Milliseconds 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. |
| children | React.ReactNode | The image and fallback parts. The root renders no instance of its own, so children mount into the surrounding parent. |
Avatar.Image
| Prop | Type | Description |
|---|---|---|
| src | string | Image source for this part. Overrides the root's src when set; otherwise the root's src is used. |
| asChild | boolean | Merge 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. |
| children | React.ReactElement | The element to render. Required when asChild is set. |
Avatar.Fallback
| Prop | Type | Description |
|---|---|---|
| asChild | boolean | Merge the derived visibility onto the single child element instead of rendering the default 40x40 circular textlabel. |
| children | React.ReactElement | The 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. |