Magic Native UI

Avatar

Image with an automatic fallback.

Installation

Terminal
npx magic-native-ui@latest add avatar

Usage

The example running in the preview above. Imports use the default aliases from components.json.

avatar-demo.tsx
import { View } from 'react-native'

import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Text } from '@/components/ui/text'

export function AvatarDemo() {
  return (
    <View className="flex-row items-center gap-3">
      <Avatar alt="Shadcn avatar">
        <AvatarImage source={{ uri: 'https://github.com/shadcn.png' }} />
        <AvatarFallback>
          <Text>CN</Text>
        </AvatarFallback>
      </Avatar>
      <Avatar alt="Fallback only">
        <AvatarFallback>
          <Text>MN</Text>
        </AvatarFallback>
      </Avatar>
    </View>
  )
}

Component source

What the CLI copies into your project. Once it is there, the code is yours to edit.

registry/ui/avatar.tsx
import * as React from 'react'
import { Image, View, type ImageProps, type ViewProps } from 'react-native'

import { TextClassContext } from '@/registry/ui/text'
import { cn } from '@/registry/lib/utils'

type AvatarStatus = 'loading' | 'loaded' | 'error'

type AvatarContextValue = {
  alt: string
  status: AvatarStatus
  setStatus: (status: AvatarStatus) => void
}

const AvatarContext = React.createContext<AvatarContextValue | null>(null)

function useAvatarContext(name: string) {
  const context = React.useContext(AvatarContext)

  if (!context) {
    throw new Error(`<${name} /> must be rendered inside an <Avatar />`)
  }

  return context
}

type AvatarProps = ViewProps & {
  /** Announced in place of the image, and used as the fallback's label. */
  alt: string
}

function Avatar({ alt, className, ...props }: AvatarProps) {
  const [status, setStatus] = React.useState<AvatarStatus>('loading')
  const value = React.useMemo(() => ({ alt, status, setStatus }), [alt, status])

  return (
    <AvatarContext.Provider value={value}>
      <View
        className={cn('relative h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
        {...props}
      />
    </AvatarContext.Provider>
  )
}

function AvatarImage({ className, onLoad, onError, ...props }: Omit<ImageProps, 'alt'>) {
  const { alt, status, setStatus } = useAvatarContext('AvatarImage')

  if (status === 'error') {
    return null
  }

  return (
    <Image
      alt={alt}
      onLoad={(event) => {
        setStatus('loaded')
        onLoad?.(event)
      }}
      onError={(event) => {
        setStatus('error')
        onError?.(event)
      }}
      className={cn('h-full w-full', className)}
      {...props}
    />
  )
}

/**
 * Sits over the image rather than beside it, so it covers the empty frame while
 * the image is still loading and disappears the moment it arrives.
 */
function AvatarFallback({ className, ...props }: ViewProps) {
  const { alt, status } = useAvatarContext('AvatarFallback')

  if (status === 'loaded') {
    return null
  }

  return (
    <TextClassContext.Provider value="text-sm font-medium text-muted-foreground">
      <View
        role="img"
        aria-label={alt}
        className={cn(
          'absolute inset-0 items-center justify-center rounded-full bg-muted',
          className
        )}
        {...props}
      />
    </TextClassContext.Provider>
  )
}

export { Avatar, AvatarFallback, AvatarImage }
export type { AvatarProps }