The Avatar component is a thumbnail representation of an entity, such as a user or an organization.
<Flex items="end" gap="5" className="my-4"> <Avatar size="sm" alt="Avatar" /> <Avatar size="md" alt="Avatar" /> <Avatar size="lg" alt="Avatar" /></Flex><Flex items="end" gap="5" className="my-4"> <Avatar size="sm" alt="Avatar" text="Ney" /> <Avatar size="md" alt="Avatar" text="Jake" /> <Avatar size="lg" alt="Avatar" text="MQ" /></Flex><Flex items="end" gap="5" className="my-4"> <Avatar size="sm" alt="Avatar" text="Ney" src="/img/neytiri.webp" /> <Avatar size="md" alt="Avatar" text="Jake" src="/img/jake.webp" /> <Avatar size="lg" alt="Avatar" text="MQ" src="/img/miles-quaritch.png" /></Flex>
Here's the <Avatar />
component in action.
import { type HTMLAttributes, forwardRef, type ImgHTMLAttributes, useRef, useEffect, useState } from 'react'import { Icon } from '#app/components/media/icon'import { cn } from '#app/utils/tailwind-merge.ts'
const AvatarRoot = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ ...props }, ref) => <div ref={ref} {...props} />)AvatarRoot.displayName = 'AvatarRoot'
const AvatarImage = forwardRef<HTMLImageElement, ImgHTMLAttributes<HTMLImageElement>>(({ alt, ...props }, ref) => <img ref={ref} alt={alt} {...props} />)AvatarImage.displayName = 'AvatarImage'
const AvatarText = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ ...props }, ref) => <span ref={ref} {...props} />)AvatarText.displayName = 'AvatarText'
export type AvatarProps = HTMLAttributes<HTMLDivElement> & { alt: string src?: string text?: string size?: 'sm' | 'md' | 'lg'}
const Avatar = forwardRef<HTMLDivElement, AvatarProps>(({ src, alt, text, size, className, ...props }, ref) => { const [isLoaded, setIsLoaded] = useState(true) // Assume image loads before JavaScript. const imageRef = useRef<HTMLImageElement>(null)
useEffect(() => { // If JavaScript loads before image if (imageRef.current) { if (!imageRef.current.complete) { // Image didn't complete loading, hide it and wait for the onLoad or onError event setIsLoaded(false) return } if (imageRef.current.naturalWidth === 0) { // Image completed but is broken (https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/complete) setIsLoaded(false) } } }, [imageRef])
const onLoad = () => { setIsLoaded(true) }
const onError = () => { setIsLoaded(false) }
return ( <AvatarRoot ref={ref} className={cn('avatar', size, className)}> {text ? <AvatarText aria-label={alt}>{text}</AvatarText> : <Icon name="avatar" label={alt} />} {src ? <AvatarImage ref={imageRef} src={src} alt={alt} onLoad={onLoad} onError={onError} className={`transition ease-in-out duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'}`} {...props} /> : null} </AvatarRoot> )})Avatar.displayName = 'Avatar'export { Avatar }
.avatar { @apply bg-muted-400 relative flex items-center justify-center overflow-hidden rounded-full text-white;
/* Sizes */ &.sm { @apply h-fluid-12 w-fluid-12; > span { @apply text-xs; } } &.md { @apply h-fluid-14 w-fluid-14; > span { @apply text-sm; } } &.lg { @apply h-fluid-15 w-fluid-15; > span { @apply text-md; } }
& > img { @apply absolute min-h-full min-w-full object-cover; }
& > span { @apply absolute flex items-center justify-center; }}