# Avatar

> An image element with a fallback for representing the user.

**BNA UI** — a React Native / Expo component library.
These components render through `react-native`, not the DOM: there are no HTML
elements, no Tailwind classes and no Radix primitives. Source is copied into your
project and imported through `@/components/ui/*`, `@/components/charts/*`,
`@/hooks/*` and `@/theme/*`. Colours come from the `useColor` hook rather than
hardcoded hex; sizing tokens (`HEIGHT`, `FONT_SIZE`, `BORDER_RADIUS`, `CORNERS`)
come from `@/theme/globals`.

- Docs: https://ui.ahmedbna.com/docs/components/avatar
- Markdown: https://ui.ahmedbna.com/docs/components/avatar.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/avatar.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/avatar.json
- Install: `npx bna-ui add avatar`
- npm dependencies: `expo-image`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `globals`, `text`, `view`, `image`
- Preview recording: https://demo.ahmedbna.com/0045-avatar-demo.PNG

---

**Example:** A basic avatar with image and fallback text

```tsx
// components/demo/avatar/avatar-demo.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import React from 'react';

export function AvatarDemo() {
  return (
    <Avatar>
      <AvatarImage
        source={{ uri: 'https://avatars.githubusercontent.com/u/99088394?v=4' }}
      />
      <AvatarFallback>AB</AvatarFallback>
    </Avatar>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add avatar
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install expo-image
```

**2.** Copy and paste the following code into your project.

```tsx
// components/ui/avatar.tsx
import { Image } from '@/components/ui/image';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import { FONT_SIZE } from '@/theme/globals';
import { ImageProps, ImageSource } from 'expo-image';
import {
  createContext,
  Dispatch,
  memo,
  SetStateAction,
  useContext,
  useState,
} from 'react';
import { TextStyle, ViewStyle } from 'react-native';

type AvatarImageStatus = 'loading' | 'loaded' | 'error';

interface AvatarContextValue {
  status: AvatarImageStatus;
  setStatus: Dispatch<SetStateAction<AvatarImageStatus>>;
}

// Connects AvatarImage's load state to AvatarFallback so the fallback shows
// automatically on error (or while there's no image at all) and hides once
// the image has actually loaded, instead of being rendered unconditionally
// by whatever the consumer puts in JSX.
const AvatarContext = createContext<AvatarContextValue | null>(null);

const useAvatarContext = () => {
  const context = useContext(AvatarContext);
  if (!context) {
    throw new Error('Avatar subcomponents must be used within an Avatar');
  }
  return context;
};

interface AvatarProps {
  children: React.ReactNode;
  size?: number;
  style?: ViewStyle;
}

export const Avatar = memo(function Avatar({
  children,
  size = 40,
  style,
}: AvatarProps) {
  const [status, setStatus] = useState<AvatarImageStatus>('loading');

  return (
    <AvatarContext.Provider value={{ status, setStatus }}>
      <View
        style={[
          {
            width: size,
            height: size,
            borderRadius: size / 2,
            overflow: 'hidden',
            position: 'relative',
          },
          style,
        ]}
      >
        {children}
      </View>
    </AvatarContext.Provider>
  );
});

interface AvatarImageProps {
  source: ImageSource;
  style?: ImageProps['style'];
}

export const AvatarImage = memo(function AvatarImage({
  source,
  style,
}: AvatarImageProps) {
  const { setStatus } = useAvatarContext();

  return (
    <Image
      source={source}
      style={[style]}
      accessibilityRole='image'
      onLoadStart={() => setStatus('loading')}
      onError={() => setStatus('error')}
      onLoadEnd={() =>
        setStatus((prev) => (prev === 'error' ? 'error' : 'loaded'))
      }
    />
  );
});

interface AvatarFallbackProps {
  children: React.ReactNode;
  style?: ViewStyle;
  textStyle?: TextStyle;
}

export const AvatarFallback = memo(function AvatarFallback({
  children,
  style,
  textStyle,
}: AvatarFallbackProps) {
  const { status } = useAvatarContext();
  const mutedColor = useColor('muted');
  const mutedForegroundColor = useColor('mutedForeground');

  if (status === 'loaded') return null;

  return (
    <View
      style={[
        {
          width: '100%',
          height: '100%',
          backgroundColor: mutedColor,
          alignItems: 'center',
          justifyContent: 'center',
        },
        style,
      ]}
    >
      <Text
        style={[
          {
            color: mutedForegroundColor,
            fontSize: FONT_SIZE,
            fontWeight: '500',
          },
          textStyle,
        ]}
      >
        {children}
      </Text>
    </View>
  );
});
```

**3.** Update the import paths to match your project setup.

## Usage

```tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
```

```tsx
<Avatar>
  <AvatarImage
    source={{ uri: 'https://avatars.githubusercontent.com/u/99088394?v=4' }}
  />
  <AvatarFallback>AB</AvatarFallback>
</Avatar>
```

## Examples

#### Default

**Example:** A basic avatar with image and fallback text

```tsx
// components/demo/avatar/avatar-demo.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import React from 'react';

export function AvatarDemo() {
  return (
    <Avatar>
      <AvatarImage
        source={{ uri: 'https://avatars.githubusercontent.com/u/99088394?v=4' }}
      />
      <AvatarFallback>AB</AvatarFallback>
    </Avatar>
  );
}
```

#### Sizes

**Example:** Avatars in different sizes

```tsx
// components/demo/avatar/avatar-sizes.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { View } from '@/components/ui/view';
import React from 'react';

export function AvatarSizes() {
  return (
    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
      <Avatar size={24}>
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/99088394?v=4',
          }}
        />
        <AvatarFallback textStyle={{ fontSize: 10 }}>AB</AvatarFallback>
      </Avatar>

      <Avatar size={32}>
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/99088394?v=4',
          }}
        />
        <AvatarFallback textStyle={{ fontSize: 12 }}>AB</AvatarFallback>
      </Avatar>

      <Avatar size={40}>
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/99088394?v=4',
          }}
        />
        <AvatarFallback>AB</AvatarFallback>
      </Avatar>

      <Avatar size={56}>
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/99088394?v=4',
          }}
        />
        <AvatarFallback textStyle={{ fontSize: 18 }}>AB</AvatarFallback>
      </Avatar>

      <Avatar size={72}>
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/99088394?v=4',
          }}
        />
        <AvatarFallback textStyle={{ fontSize: 24 }}>AB</AvatarFallback>
      </Avatar>
    </View>
  );
}
```

#### Fallback Only

**Example:** Avatars with fallback text when no image is provided

```tsx
// components/demo/avatar/avatar-fallback.tsx
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { View } from '@/components/ui/view';
import React from 'react';

export function AvatarFallbackDemo() {
  return (
    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
      <Avatar>
        <AvatarFallback>JD</AvatarFallback>
      </Avatar>

      <Avatar>
        <AvatarFallback>AB</AvatarFallback>
      </Avatar>

      <Avatar>
        <AvatarFallback>MK</AvatarFallback>
      </Avatar>

      <Avatar>
        <AvatarFallback>SL</AvatarFallback>
      </Avatar>
    </View>
  );
}
```

#### Custom Styling

**Example:** Avatars with custom styling and colors

```tsx
// components/demo/avatar/avatar-styled.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { View } from '@/components/ui/view';
import React from 'react';

export function AvatarStyled() {
  return (
    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
      <Avatar
        size={56}
        style={{
          borderWidth: 3,
          borderColor: '#3b82f6',
        }}
      >
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/99088394?v=4',
          }}
        />
        <AvatarFallback
          style={{ backgroundColor: '#3b82f6' }}
          textStyle={{ color: 'white', fontWeight: 'bold' }}
        >
          AB
        </AvatarFallback>
      </Avatar>

      <Avatar
        size={56}
        style={{
          borderWidth: 3,
          borderColor: '#10b981',
        }}
      >
        <AvatarFallback
          style={{ backgroundColor: '#10b981' }}
          textStyle={{ color: 'white', fontWeight: 'bold' }}
        >
          BNA
        </AvatarFallback>
      </Avatar>

      <Avatar
        size={56}
        style={{
          borderWidth: 3,
          borderColor: '#f59e0b',
        }}
      >
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/12504344?s=200&v=4',
          }}
        />
        <AvatarFallback
          style={{ backgroundColor: '#f59e0b' }}
          textStyle={{ color: 'white', fontWeight: 'bold' }}
        >
          EX
        </AvatarFallback>
      </Avatar>
    </View>
  );
}
```

#### Group

**Example:** Multiple avatars arranged in a group layout

```tsx
// components/demo/avatar/avatar-group.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { View } from '@/components/ui/view';
import React from 'react';

export function AvatarGroup() {
  return (
    <View style={{ flexDirection: 'row', alignItems: 'center' }}>
      <Avatar
        size={48}
        style={{
          borderWidth: 2,
          borderColor: 'white',
          zIndex: 4,
        }}
      >
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/99088394?v=4',
          }}
        />
        <AvatarFallback>AB</AvatarFallback>
      </Avatar>

      <Avatar
        size={48}
        style={{
          borderWidth: 2,
          borderColor: 'white',
          marginLeft: -12,
          zIndex: 3,
        }}
      >
        <AvatarImage
          source={{
            uri: 'https://reactnative.dev/img/header_logo.svg',
          }}
        />
        <AvatarFallback>AB</AvatarFallback>
      </Avatar>

      <Avatar
        size={48}
        style={{
          borderWidth: 2,
          borderColor: 'white',
          marginLeft: -12,
          zIndex: 2,
        }}
      >
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/12504344?s=200&v=4',
          }}
        />
        <AvatarFallback>EX</AvatarFallback>
      </Avatar>

      <Avatar
        size={48}
        style={{
          borderWidth: 2,
          borderColor: 'white',
          marginLeft: -12,
          zIndex: 1,
        }}
      >
        <AvatarFallback
          style={{ backgroundColor: '#6b7280' }}
          textStyle={{ color: 'white', fontSize: 12 }}
        >
          +5
        </AvatarFallback>
      </Avatar>
    </View>
  );
}
```

#### With Status

**Example:** Avatars with online/offline status indicators

```tsx
// components/demo/avatar/avatar-status.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { View } from '@/components/ui/view';
import React from 'react';

export function AvatarStatus() {
  return (
    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 24 }}>
      <View style={{ position: 'relative' }}>
        <Avatar size={56}>
          <AvatarImage
            source={{
              uri: 'https://avatars.githubusercontent.com/u/99088394?v=4',
            }}
          />
          <AvatarFallback>AB</AvatarFallback>
        </Avatar>
        <View
          style={{
            position: 'absolute',
            bottom: 0,
            right: 0,
            width: 16,
            height: 16,
            borderRadius: 8,
            backgroundColor: '#10b981',
            borderWidth: 2,
            borderColor: 'white',
          }}
        />
      </View>

      <View style={{ position: 'relative' }}>
        <Avatar size={56}>
          <AvatarFallback>BNA</AvatarFallback>
        </Avatar>
        <View
          style={{
            position: 'absolute',
            bottom: 0,
            right: 0,
            width: 16,
            height: 16,
            borderRadius: 8,
            backgroundColor: '#ef4444',
            borderWidth: 2,
            borderColor: 'white',
          }}
        />
      </View>

      <View style={{ position: 'relative' }}>
        <Avatar size={56}>
          <AvatarImage
            source={{
              uri: 'https://avatars.githubusercontent.com/u/12504344?s=200&v=4',
            }}
          />
          <AvatarFallback>EX</AvatarFallback>
        </Avatar>
        <View
          style={{
            position: 'absolute',
            bottom: 0,
            right: 0,
            width: 16,
            height: 16,
            borderRadius: 8,
            backgroundColor: '#f59e0b',
            borderWidth: 2,
            borderColor: 'white',
          }}
        />
      </View>
    </View>
  );
}
```

#### Bordered

**Example:** Avatars with custom borders and shadows

```tsx
// components/demo/avatar/avatar-bordered.tsx
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { View } from '@/components/ui/view';
import React from 'react';

export function AvatarBordered() {
  return (
    <View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
      <Avatar
        size={64}
        style={{
          borderWidth: 4,
          borderColor: '#3b82f6',
          shadowColor: '#3b82f6',
          shadowOffset: { width: 0, height: 4 },
          shadowOpacity: 0.3,
          shadowRadius: 8,
          elevation: 8,
        }}
      >
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/99088394?v=4',
          }}
        />
        <AvatarFallback>AB</AvatarFallback>
      </Avatar>

      <Avatar
        size={64}
        style={{
          borderWidth: 4,
          borderColor: '#10b981',
          shadowColor: '#10b981',
          shadowOffset: { width: 0, height: 4 },
          shadowOpacity: 0.3,
          shadowRadius: 8,
          elevation: 8,
        }}
      >
        <AvatarFallback>BNA</AvatarFallback>
      </Avatar>

      <Avatar
        size={64}
        style={{
          borderWidth: 4,
          borderColor: '#f59e0b',
          shadowColor: '#f59e0b',
          shadowOffset: { width: 0, height: 4 },
          shadowOpacity: 0.3,
          shadowRadius: 8,
          elevation: 8,
        }}
      >
        <AvatarImage
          source={{
            uri: 'https://avatars.githubusercontent.com/u/12504344?s=200&v=4',
          }}
        />
        <AvatarFallback>EX</AvatarFallback>
      </Avatar>
    </View>
  );
}
```

## API Reference

### Avatar

The container component that wraps the avatar image and fallback.

| Prop       | Type        | Default | Description                                         |
| ---------- | ----------- | ------- | --------------------------------------------------- |
| `children` | `ReactNode` | -       | The avatar image and fallback components.           |
| `size`     | `number`    | `40`    | The size of the avatar in pixels.                   |
| `style`    | `ViewStyle` | -       | Additional styles to apply to the avatar container. |

### AvatarImage

The image component that displays the user's avatar.

| Prop     | Type               | Description                              |
| -------- | ------------------ | ---------------------------------------- |
| `source` | `ImageSource`      | The image source for the avatar.         |
| `style`  | `ImageProps.style` | Additional styles to apply to the image. |

### AvatarFallback

The fallback component that displays when the image fails to load or is not provided.

| Prop        | Type        | Description                                           |
| ----------- | ----------- | ----------------------------------------------------- |
| `children`  | `ReactNode` | The fallback content (usually initials or text).      |
| `style`     | `ViewStyle` | Additional styles to apply to the fallback container. |
| `textStyle` | `TextStyle` | Additional styles to apply to the fallback text.      |

## Accessibility

The Avatar component is built with accessibility in mind:

- Uses semantic structure for screen readers
- Fallback text provides alternative content when images fail to load
- Proper contrast ratios for text fallbacks
- Supports dynamic text sizing
