shadimage
indiacn
uiavatarsource: ok · refreshed

Avatar

Person or entity avatar with picture, initials, or icon fallback. Four sizes, circular or rectangular.

npx shadcn add "https://indiacn.in/r/avatar.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@radix-ui/react-avatar

Registry dependencies

https://indiacn.in/r/theme.json

Files1 · source cached 10/8/2026, 4:45:38 AM

components/ui/avatar.tsx
'use client';

import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cva, type VariantProps } from 'class-variance-authority';
import { Children, ComponentProps, ReactNode, createContext, useContext, useMemo } from 'react';

import { cn } from '@/lib/utils';

/*
 * UX4G avatar: 24/32/40/48px, circular or 8px-rounded, primary-100 on
 * primary-800. Initials scale with the box: 11/12/14/16px.
 */
const AVATAR_VARIANTS = cva(
  'relative inline-flex shrink-0 items-center justify-center overflow-hidden align-middle font-medium select-none',
  {
    variants: {
      size: {
        sm: 'size-6 text-[11px] leading-4 tracking-[0.5px]',
        md: 'size-8 text-xs leading-4 tracking-[0.5px]',
        lg: 'size-10 text-sm leading-5 tracking-[0.1px]',
        xl: 'size-12 text-base leading-6 tracking-[0.15px]',
      },
      shape: {
        circular: 'rounded-full',
        rectangular: 'rounded-md',
      },
    },
    defaultVariants: {
      size: 'md',
      shape: 'circular',
    },
  },
);

type TAvatarSize = NonNullable<VariantProps<typeof AVATAR_VARIANTS>['size']>;
type TAvatarShape = NonNullable<VariantProps<typeof AVATAR_VARIANTS>['shape']>;

const AVATAR_CONTEXT = createContext<TAvatarSize>('md');

interface IAvatarGroupContext {
  size: TAvatarSize;
  shape: TAvatarShape;
  inGroup: boolean;
}

const GROUP_CONTEXT = createContext<IAvatarGroupContext>({
  size: 'md',
  shape: 'circular',
  inGroup: false,
});

const GROUP_OVERLAP: Record<TAvatarSize, string> = {
  sm: '-ml-1.5',
  md: '-ml-2',
  lg: '-ml-2.5',
  xl: '-ml-3',
};

interface IAvatarProps
  extends ComponentProps<typeof AvatarPrimitive.Root>,
    VariantProps<typeof AVATAR_VARIANTS> {}

/** Avatar container. Wrap an AvatarImage and an AvatarFallback. */
function Avatar({ className, size, shape, ...props }: IAvatarProps) {
  const { size: groupSize, shape: groupShape, inGroup } = useContext(GROUP_CONTEXT);
  const effectiveSize = size ?? (inGroup ? groupSize : 'md');
  const effectiveShape = shape ?? (inGroup ? groupShape : 'circular');

  return (
    <AVATAR_CONTEXT.Provider value={effectiveSize}>
      <AvatarPrimitive.Root
        className={cn(
          AVATAR_VARIANTS({ size: effectiveSize, shape: effectiveShape }),
          inGroup && 'ring-neutral-0 ring-2',
          className,
        )}
        {...props}
      />
    </AVATAR_CONTEXT.Provider>
  );
}

/** Avatar picture. Falls back automatically if the image fails to load. */
function AvatarImage({ className, ...props }: ComponentProps<typeof AvatarPrimitive.Image>) {
  return <AvatarPrimitive.Image className={cn('size-full object-cover', className)} {...props} />;
}

/** Shown while the image loads or when there is none. Holds initials or an icon. */
function AvatarFallback({ className, ...props }: ComponentProps<typeof AvatarPrimitive.Fallback>) {
  const size = useContext(AVATAR_CONTEXT);
  const iconSize = {
    sm: '[&>svg]:size-3.5',
    md: '[&>svg]:size-4',
    lg: '[&>svg]:size-5',
    xl: '[&>svg]:size-6',
  };

  return (
    <AvatarPrimitive.Fallback
      className={cn(
        'bg-primary-100 text-primary-800 flex size-full items-center justify-center uppercase',
        iconSize[size],
        className,
      )}
      {...props}
    />
  );
}

interface IAvatarGroupProps {
  children: ReactNode;
  size?: TAvatarSize;
  shape?: TAvatarShape;
  /** Maximum avatars shown before a +N overflow badge appears. Defaults to 5. */
  max?: number;
  className?: string;
}

/** Stacks multiple avatars with overlap. Excess avatars beyond `max` render as a +N badge. */
function AvatarGroup({
  size = 'md',
  shape = 'circular',
  max = 5,
  className,
  children,
}: IAvatarGroupProps) {
  const all = Children.toArray(children);
  const visible = all.slice(0, max);
  const overflow = all.length - visible.length;
  const groupContext = useMemo(() => ({ size, shape, inGroup: true }), [size, shape]);

  return (
    <GROUP_CONTEXT.Provider value={groupContext}>
      <div className={cn('flex items-center', className)}>
        {visible.map((child, i) => (
          <div key={i} className={cn('flex', i > 0 && GROUP_OVERLAP[size])}>
            {child}
          </div>
        ))}
        {overflow > 0 && (
          <div
            className={cn(
              GROUP_OVERLAP[size],
              AVATAR_VARIANTS({ size, shape }),
              'ring-neutral-0 ring-2',
            )}
          >
            <div className='flex size-full items-center justify-center bg-neutral-100 font-medium text-neutral-700'>
              +{overflow}
            </div>
          </div>
        )}
      </div>
    </GROUP_CONTEXT.Provider>
  );
}

export type { TAvatarSize, TAvatarShape };

export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AVATAR_VARIANTS };

More from indiacn

Browse all components in this library →