# Avatar

> Round user image (expo-image) with fallback initials, in sizes sm, md and lg.

Component · display. Registry item `avatar`, shown in the vega Style. JSON: https://nativecn.dev/r/styles/vega/avatar.json

## Installation

```sh
npx nativecn-cli@latest add avatar
```

Also installs: [theme](https://nativecn.dev/docs/theme/theme), [text](https://nativecn.dev/docs/components/text).

Packages: `expo-image`.

## Usage

### Avatar demo

Avatars in every size, with an image and with initials only.

```tsx
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

### {components}/avatar.tsx

```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>
  );
}
```
