Avatar
Round user image (expo-image) with fallback initials, in sizes sm, md and lg.
Componentdisplay
Style
Light
Dark
Your app is ready.
Inter body text, Inter headings.
smmdlgxl
you@example.com
Continue
a0: Vega, neutral, neutral accent, default radius, Inter.Change PresetInstallation
$
npx nativecn-cli@latest add avatarPackages: expo-image.
Usage
Avatar demo
Avatars in every size, with an image and with initials only.
import { View } from "react-native";
import { Avatar } from "@/components/avatar";
import { createStyles } from "@/theme";
export default function AvatarDemo() {
const styles = useStyles();
return (
<View style={styles.row}>
<Avatar size="sm" src="https://github.com/expo.png" fallback="EX" alt="Expo" />
<Avatar src="https://github.com/shadcn.png" fallback="CN" alt="shadcn" />
{/* No image: the initials show. */}
<Avatar size="lg" fallback="JD" alt="Jane Doe" />
</View>
);
}
const useStyles = createStyles((t) => ({
row: { flexDirection: "row", alignItems: "center", gap: t.spacing[3] },
}));Avatar demo
Avatars in every size, with an image and with initials only.
import { View } from "react-native";
import { Avatar } from "@/components/avatar";
import { createStyles } from "@/theme";
export default function AvatarDemo() {
const styles = useStyles();
return (
<View style={styles.row}>
<Avatar size="sm" src="https://github.com/expo.png" fallback="EX" alt="Expo" />
<Avatar src="https://github.com/shadcn.png" fallback="CN" alt="shadcn" />
{/* No image: the initials show. */}
<Avatar size="lg" fallback="JD" alt="Jane Doe" />
</View>
);
}
const useStyles = createStyles((t) => ({
row: { flexDirection: "row", alignItems: "center", gap: t.spacing[3] },
}));Props
| Prop | Type and description |
|---|---|
src | string: the image URI |
fallback | string: initials shown while loading, on error or without src (e.g. "JD") |
size | "sm" | "md" (default) | "lg": Vega 32 / 40 / 64, Nova 24 / 32 / 48 |
alt | string: who it shows; without it the avatar is decorative (hidden from screen readers) |
style | merged last |
Accessibility
- With alt it has role img and is read with that name; without alt it is decorative and hidden.
- Pass alt unless the person's name is already next to the avatar.
- The fallback initials never follow the OS font size, so they stay inside the circle.
Notes
expo-image is SDK-pinned (npx expo install expo-image). The fallback sits behind the image, so it shows while the image loads and stays when it fails.
Source
The Vega Style's code, exactly as add copies it into your app.
{components}/avatar.tsx
import { Image } from "expo-image";
import { useState, type Ref } from "react";
import { View, type ViewProps } from "react-native";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";
/** The Style sets each size before Scale: Vega sm 32 · md 40 · lg 64, Nova sm 24 · md 32 · lg 48. */
export type AvatarSize = "sm" | "md" | "lg";
export type AvatarProps = Omit<ViewProps, "children"> & {
/** The image URI. While it loads, or if it fails, the `fallback` shows instead. */
src?: string;
/** Initials (or any short text) shown when there is no image, e.g. `"JD"`. */
fallback?: string;
/** Default `md`. */
size?: AvatarSize;
/**
* Who the avatar shows, e.g. `"Jane Doe"`. Makes it an image for screen readers; without it
* the avatar is decorative and hidden (the name is usually already next to it).
*/
alt?: string;
ref?: Ref<View>;
};
/** Initials take 40% of the avatar's side; the line height leaves room for descenders. */
const initials = (side: number) => ({ fontSize: side * 0.4, lineHeight: side * 0.5 });
const useStyles = createStyles((t) => {
const md = { width: t.scaleValue(40), height: t.scaleValue(40) };
const sm = { width: t.scaleValue(32), height: t.scaleValue(32) };
const lg = { width: t.scaleValue(64), height: t.scaleValue(64) };
return {
root: {
...md,
borderRadius: t.radius.full,
backgroundColor: t.colors.muted,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
},
sm,
md: {},
lg,
fallbackSm: initials(sm.height),
fallbackMd: initials(md.height),
fallbackLg: initials(lg.height),
image: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 },
};
});
const fallbackStyle = { sm: "fallbackSm", md: "fallbackMd", lg: "fallbackLg" } as const;
/**
* A round user image loaded with expo-image, with the `fallback` initials behind it while it
* loads or when it fails. Decorative unless `alt` is given. `style` is merged last.
*/
export function Avatar({ src, fallback, size = "md", alt, style, ...props }: AvatarProps) {
const styles = useStyles();
// Remember which URI failed rather than a flag, so a recycled row (FlashList) with a new
// `src` tries again without an effect.
const [failed, setFailed] = useState<string>();
const showImage = !!src && src !== failed;
const decorative = !alt;
return (
<View
role={decorative ? undefined : "img"}
aria-label={alt}
aria-hidden={decorative}
accessible={!decorative}
importantForAccessibility={decorative ? "no-hide-descendants" : "yes"}
style={[styles.root, styles[size], style]}
{...props}
>
{fallback ? (
<Text
variant="label"
color="mutedForeground"
numberOfLines={1}
allowFontScaling={false}
style={styles[fallbackStyle[size]]}
>
{fallback}
</Text>
) : null}
{showImage ? (
<Image
source={{ uri: src }}
contentFit="cover"
accessible={false}
onError={() => setFailed(src)}
style={styles.image}
testID={props.testID ? `${props.testID}-image` : undefined}
/>
) : null}
</View>
);
}{components}/avatar.tsx
import { Image } from "expo-image";
import { useState, type Ref } from "react";
import { View, type ViewProps } from "react-native";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";
/** The Style sets each size before Scale: Vega sm 32 · md 40 · lg 64, Nova sm 24 · md 32 · lg 48. */
export type AvatarSize = "sm" | "md" | "lg";
export type AvatarProps = Omit<ViewProps, "children"> & {
/** The image URI. While it loads, or if it fails, the `fallback` shows instead. */
src?: string;
/** Initials (or any short text) shown when there is no image, e.g. `"JD"`. */
fallback?: string;
/** Default `md`. */
size?: AvatarSize;
/**
* Who the avatar shows, e.g. `"Jane Doe"`. Makes it an image for screen readers; without it
* the avatar is decorative and hidden (the name is usually already next to it).
*/
alt?: string;
ref?: Ref<View>;
};
/** Initials take 40% of the avatar's side; the line height leaves room for descenders. */
const initials = (side: number) => ({ fontSize: side * 0.4, lineHeight: side * 0.5 });
const useStyles = createStyles((t) => {
const md = { width: t.scaleValue(32), height: t.scaleValue(32) };
const sm = { width: t.scaleValue(24), height: t.scaleValue(24) };
const lg = { width: t.scaleValue(48), height: t.scaleValue(48) };
return {
root: {
...md,
borderRadius: t.radius.full,
backgroundColor: t.colors.muted,
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
},
sm,
md: {},
lg,
fallbackSm: initials(sm.height),
fallbackMd: initials(md.height),
fallbackLg: initials(lg.height),
image: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 },
};
});
const fallbackStyle = { sm: "fallbackSm", md: "fallbackMd", lg: "fallbackLg" } as const;
/**
* A round user image loaded with expo-image, with the `fallback` initials behind it while it
* loads or when it fails. Decorative unless `alt` is given. `style` is merged last.
*/
export function Avatar({ src, fallback, size = "md", alt, style, ...props }: AvatarProps) {
const styles = useStyles();
// Remember which URI failed rather than a flag, so a recycled row (FlashList) with a new
// `src` tries again without an effect.
const [failed, setFailed] = useState<string>();
const showImage = !!src && src !== failed;
const decorative = !alt;
return (
<View
role={decorative ? undefined : "img"}
aria-label={alt}
aria-hidden={decorative}
accessible={!decorative}
importantForAccessibility={decorative ? "no-hide-descendants" : "yes"}
style={[styles.root, styles[size], style]}
{...props}
>
{fallback ? (
<Text
variant="label"
color="mutedForeground"
numberOfLines={1}
allowFontScaling={false}
style={styles[fallbackStyle[size]]}
>
{fallback}
</Text>
) : null}
{showImage ? (
<Image
source={{ uri: src }}
contentFit="cover"
accessible={false}
onError={() => setFailed(src)}
style={styles.image}
testID={props.testID ? `${props.testID}-image` : undefined}
/>
) : null}
</View>
);
}
