TWIGS

Avatar

An image element with a fallback for representing a user.

Playground

SJGBXL
SJGBXL

Installation

Install in any React + Tailwind project:

$npx shadcn@latest add https://twigs.globirdenergy.com.au/r/avatar.json

Source

The exact file the registry serves to consumers.

'use client';

import * as React from 'react';
import { cn } from '@/lib/utils';

type AvatarContextValue = { state: 'idle' | 'loaded' | 'error'; setState: (s: 'idle' | 'loaded' | 'error') => void };
const AvatarContext = React.createContext<AvatarContextValue | null>(null);

// Spec source: Figma "APP Re-design Phase 1" → page "Avatar ✅" (40x40, pill)
function Avatar({ className, ...props }: React.ComponentProps<'span'>) {
  const [state, setState] = React.useState<'idle' | 'loaded' | 'error'>('idle');
  return (
    <AvatarContext.Provider value={{ state, setState }}>
      <span
        data-slot="avatar"
        className={cn('relative flex size-10 shrink-0 overflow-hidden rounded-full', className)}
        {...props}
      />
    </AvatarContext.Provider>
  );
}

function AvatarImage({ className, onLoad, onError, ...props }: React.ComponentProps<'img'>) {
  const ctx = React.useContext(AvatarContext);
  return ctx?.state === 'error' ? null : (
    <img
      data-slot="avatar-image"
      className={cn('aspect-square size-full', className)}
      onLoad={(e) => {
        ctx?.setState('loaded');
        onLoad?.(e);
      }}
      onError={(e) => {
        ctx?.setState('error');
        onError?.(e);
      }}
      {...props}
    />
  );
}

function AvatarFallback({ className, ...props }: React.ComponentProps<'span'>) {
  const ctx = React.useContext(AvatarContext);
  if (ctx?.state === 'loaded') return null;
  return (
    <span
      data-slot="avatar-fallback"
      className={cn(
        'bg-muted flex size-full items-center justify-center rounded-full text-sm font-medium',
        className,
      )}
      {...props}
    />
  );
}

export { Avatar, AvatarImage, AvatarFallback };