shadimage
rawkitui
uiavatarsource: ok · refreshed

Avatar

Rounded profile image with resilient initials fallback and status dot.

npx shadcn add "https://rawkitui.zerodegree.tech/r/avatar.json"
free

Where it goes

statsprofile

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
clsxtailwind-merge

Files1 · source cached 10/8/2026, 1:09:26 AM

components/ui/avatar.tsx
import * as React from 'react'
import { User } from 'lucide-react'
import { cn } from '@/lib/utils'

export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
  src?: string
  alt?: string
  fallback?: React.ReactNode
  icon?: React.ReactNode
  size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
  variant?: 'purple' | 'yellow' | 'pink' | 'orange' | 'cyan' | 'lime' | 'mint' | 'zinc'
  shape?: 'circle' | 'square'
  status?: 'online' | 'busy' | 'offline' | 'away'
  badge?: string | number
  name?: React.ReactNode
  description?: React.ReactNode
}

const sizeClasses = {
  xs: 'h-7 w-7 text-[10px]',
  sm: 'h-9 w-9 text-xs',
  md: 'h-11 w-11 text-sm',
  lg: 'h-14 w-14 text-base',
  xl: 'h-18 w-18 text-xl',
  '2xl': 'h-24 w-24 text-3xl',
}

const iconSizeClasses = {
  xs: 'h-3.5 w-3.5',
  sm: 'h-4 w-4',
  md: 'h-5 w-5',
  lg: 'h-7 w-7',
  xl: 'h-9 w-9',
  '2xl': 'h-12 w-12',
}

const variantClasses = {
  purple: 'bg-rk-lavender text-rk-ink',
  yellow: 'bg-rk-primary text-rk-ink',
  pink: 'bg-rk-pink text-rk-ink',
  orange: 'bg-rk-secondary text-rk-ink',
  cyan: 'bg-rk-sky text-rk-ink',
  lime: 'bg-[#A3E635] text-rk-ink',
  mint: 'bg-rk-success text-rk-ink',
  zinc: 'bg-rk-canvas text-rk-ink',
}

const statusClasses = {
  online: 'bg-rk-success',
  busy: 'bg-rk-error',
  offline: 'bg-[#9CA3AF]',
  away: 'bg-rk-primary-hover',
}

export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
  (
    {
      src,
      alt = '',
      fallback,
      icon,
      size = 'md',
      variant = 'purple',
      shape = 'circle',
      status,
      badge,
      name,
      description,
      className,
      ...props
    },
    ref
  ) => {
    const [hasError, setHasError] = React.useState(false)

    const renderContent = () => {
      if (src && !hasError) {
        return (
          <img
            src={src}
            alt={alt}
            onError={() => setHasError(true)}
            className="h-full w-full object-cover"
          />
        )
      }

      if (icon) {
        return React.isValidElement(icon)
          ? React.cloneElement(icon as React.ReactElement<{ className?: string }>, {
              className: cn(iconSizeClasses[size], (icon.props as { className?: string })?.className),
            })
          : icon
      }

      if (fallback) {
        return <span aria-hidden={Boolean(alt)}>{fallback}</span>
      }

      if (alt) {
        return <span aria-hidden={Boolean(alt)}>{alt.slice(0, 2).toUpperCase()}</span>
      }

      return <User className={iconSizeClasses[size]} />
    }

    const avatarBox = (
      <div className="relative inline-flex shrink-0">
        <div
          className={cn(
            'flex items-center justify-center overflow-hidden font-display font-black rk-border rk-shadow-sm transition-transform hover:-translate-y-0.5',
            shape === 'circle' ? 'rounded-full' : 'rounded-2xl',
            sizeClasses[size],
            variantClasses[variant]
          )}
        >
          {renderContent()}
        </div>

        {status && (
          <span
            aria-label={status}
            className={cn(
              'absolute bottom-0 right-0 rounded-full border-2 border-rk-ink rk-shadow-xs',
              size === 'xs' || size === 'sm' ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5',
              statusClasses[status]
            )}
          />
        )}

        {badge !== undefined && (
          <span className="absolute -top-1.5 -right-1.5 flex h-5 min-w-[20px] items-center justify-center rounded-full border-2 border-rk-ink bg-rk-pink px-1 font-mono text-[10px] font-black text-rk-ink rk-shadow-xs">
            {badge}
          </span>
        )}
      </div>
    )

    if (name || description) {
      return (
        <div ref={ref} className={cn('inline-flex items-center gap-3', className)} {...props}>
          {avatarBox}
          <div className="flex flex-col text-left">
            {name && <span className="font-display text-sm font-extrabold text-rk-ink leading-snug">{name}</span>}
            {description && <span className="font-sans text-xs font-semibold text-rk-ink/60">{description}</span>}
          </div>
        </div>
      )
    }

    return (
      <div ref={ref} className={cn('inline-flex shrink-0', className)} {...props}>
        {avatarBox}
      </div>
    )
  }
)
Avatar.displayName = 'Avatar'

export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
  children: React.ReactNode
  max?: number
}

export const AvatarGroup: React.FC<AvatarGroupProps> = ({ children, max, className, ...props }) => {
  const childrenArray = React.Children.toArray(children)
  const displayChildren = max ? childrenArray.slice(0, max) : childrenArray
  const overflowCount = max ? childrenArray.length - max : 0

  return (
    <div className={cn('flex items-center -space-x-3.5', className)} {...props}>
      {displayChildren.map((child, index) => (
        <div key={index} className="relative z-10 transition-transform hover:z-20 hover:-translate-y-1">
          {child}
        </div>
      ))}
      {overflowCount > 0 && (
        <div className="relative z-10 flex h-11 w-11 items-center justify-center rounded-full bg-rk-primary font-mono text-xs font-black text-rk-ink rk-border rk-shadow-sm">
          +{overflowCount}
        </div>
      )}
    </div>
  )
}

More from rawkitui

Browse all components in this library →