Avatar
Display user profiles. Robust fallbacks. Build trust visually.
IFIgnacio Figueroa
Signed in as owner
1'use client';23import { Avatar } from '@/components/ui/avatar';45export function Default() {6 return (7 <div className="flex items-center gap-3">8 <Avatar>9 <Avatar.Image src="https://github.com/figueroaignacio.png" alt="Ignacio Figueroa" />10 <Avatar.Fallback>IF</Avatar.Fallback>11 </Avatar>12 <div>13 <p className="text-sm font-medium">Ignacio Figueroa</p>14 <p className="text-muted-foreground text-xs">Signed in as owner</p>15 </div>16 </div>17 );18}
Installation
Copy and paste the following code into your project.
1'use client';23import { cva, type VariantProps } from 'class-variance-authority';4import * as React from 'react';5import { cn } from '../lib/cn';67const avatarVariants = cva('relative flex shrink-0 overflow-hidden rounded-full', {8 variants: {9 size: {10 sm: 'h-8 w-8',11 md: 'h-10 w-10',12 lg: 'h-12 w-12',13 },14 },15 defaultVariants: {16 size: 'md',17 },18});1920interface AvatarContextValue {21 status: 'loading' | 'loaded' | 'error';22 setStatus: (status: 'loading' | 'loaded' | 'error') => void;23}2425const AvatarContext = React.createContext<AvatarContextValue | null>(null);2627function useAvatarContext() {28 const context = React.use(AvatarContext);29 if (!context) throw new Error('Avatar components must be used within Avatar');30 return context;31}3233export interface AvatarProps34 extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof avatarVariants> {}3536const AvatarRoot = ({ className, size, ...props }: AvatarProps) => {37 const [status, setStatus] = React.useState<'loading' | 'loaded' | 'error'>('loading');38 const contextValue = React.useMemo<AvatarContextValue>(() => ({ status, setStatus }), [status]);3940 return (41 <AvatarContext value={contextValue}>42 <span className={cn(avatarVariants({ size }), className)} {...props} />43 </AvatarContext>44 );45};46AvatarRoot.displayName = 'Avatar';4748export interface AvatarImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {49 onLoadingStatusChange?: (status: 'loaded' | 'error') => void;50}5152const AvatarImage = ({53 className,54 src,55 onLoad,56 onError,57 onLoadingStatusChange,58 ...props59}: AvatarImageProps) => {60 const { status, setStatus } = useAvatarContext();61 const imgRef = React.useRef<HTMLImageElement>(null);62 const onLoadingStatusChangeRef = React.useRef(onLoadingStatusChange);6364 React.useEffect(() => {65 onLoadingStatusChangeRef.current = onLoadingStatusChange;66 });6768 React.useEffect(() => {69 if (!src) {70 setStatus('error');71 return;72 }73 const img = imgRef.current;74 if (img?.complete) {75 const next = img.naturalWidth > 0 ? 'loaded' : 'error';76 setStatus(next);77 onLoadingStatusChangeRef.current?.(next);78 return;79 }80 setStatus('loading');81 }, [src, setStatus]);8283 if (status === 'error') return null;8485 return (86 <img87 ref={imgRef}88 src={src}89 alt=""90 data-status={status}91 className={cn('absolute inset-0 size-full object-cover', className)}92 onLoad={(e) => {93 setStatus('loaded');94 onLoadingStatusChange?.('loaded');95 onLoad?.(e);96 }}97 onError={(e) => {98 setStatus('error');99 onLoadingStatusChange?.('error');100 onError?.(e);101 }}102 {...props}103 />104 );105};106AvatarImage.displayName = 'AvatarImage';107108const AvatarFallback = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {109 const { status } = useAvatarContext();110 if (status === 'loaded') return null;111 return (112 <span113 className={cn(114 'bg-muted flex h-full w-full items-center justify-center rounded-full text-sm font-medium',115 className,116 )}117 {...props}118 />119 );120};121AvatarFallback.displayName = 'AvatarFallback';122123const AvatarGroup = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (124 <div125 className={cn('*:ring-background flex items-center -space-x-2 *:ring-2', className)}126 {...props}127 />128);129AvatarGroup.displayName = 'AvatarGroup';130131const Avatar = Object.assign(AvatarRoot, {132 Image: AvatarImage,133 Fallback: AvatarFallback,134 Group: AvatarGroup,135});136137export { Avatar, avatarVariants };
Update the import paths to match your project structure.
Anatomy
1import { Avatar } from '@/components/ui/avatar';
1<Avatar>2 <Avatar.Image src="https://github.com/shadcn.png" />3 <Avatar.Fallback>CN</Avatar.Fallback>4</Avatar>
Composition
Use the following composition to build an
Avatar:Avatarโโโ Avatar.Imageโโโ Avatar.Fallback
Variants
Default
IFIgnacio Figueroa
Signed in as owner
1'use client';23import { Avatar } from '@/components/ui/avatar';45export function Default() {6 return (7 <div className="flex items-center gap-3">8 <Avatar>9 <Avatar.Image src="https://github.com/figueroaignacio.png" alt="Ignacio Figueroa" />10 <Avatar.Fallback>IF</Avatar.Fallback>11 </Avatar>12 <div>13 <p className="text-sm font-medium">Ignacio Figueroa</p>14 <p className="text-muted-foreground text-xs">Signed in as owner</p>15 </div>16 </div>17 );18}
Sizes
You can change the size via the
size prop.
NVNicolas Vazquez
commented on checkout-flow, 2m ago
MZManuel Zarraga
Engineering, joined Feb 2024
IFIgnacio Figueroa
Workspace owner, acmestudio.dev
1'use client';23import { Avatar } from '@/components/ui/avatar';45const rows = [6 {7 size: 'sm' as const,8 src: 'https://github.com/nicvazquezdev.png',9 name: 'Nicolas Vazquez',10 detail: 'commented on checkout-flow, 2m ago',11 },12 {13 size: 'md' as const,14 src: 'https://github.com/ManuZarraga.png',15 name: 'Manuel Zarraga',16 detail: 'Engineering, joined Feb 2024',17 },18 {19 size: 'lg' as const,20 src: 'https://github.com/figueroaignacio.png',21 name: 'Ignacio Figueroa',22 detail: 'Workspace owner, acmestudio.dev',23 },24];2526export function Sizes() {27 return (28 <div className="flex w-full max-w-sm flex-col gap-5">29 {rows.map((row) => (30 <div key={row.size} className="flex items-center gap-3">31 <Avatar size={row.size}>32 <Avatar.Image src={row.src} alt={row.name} />33 <Avatar.Fallback>34 {row.name35 .split(' ')36 .map((part) => part.charAt(0))37 .join('')}38 </Avatar.Fallback>39 </Avatar>40 <div className="min-w-0">41 <p className="truncate text-sm font-medium">{row.name}</p>42 <p className="text-muted-foreground truncate text-xs">{row.detail}</p>43 </div>44 </div>45 ))}46 </div>47 );48}
With Fallback
If the image takes time to load or fails, the fallback text (usually initials) will be displayed.
Pending invites
Camila Ortiz
camila@acmestudio.dev
TI
InvitedTomas Iglesias
tomas@acmestudio.dev
1'use client';23import { Avatar } from '@/components/ui/avatar';4import { Badge } from '@/components/ui/badge';56const invites = [7 {8 src: 'https://github.com/figueroaignacio.pn',9 name: 'Camila Ortiz',10 email: 'camila@acmestudio.dev',11 initials: 'CO',12 initialsClassName: undefined,13 },14 {15 src: undefined,16 name: 'Tomas Iglesias',17 email: 'tomas@acmestudio.dev',18 initials: 'TI',19 initialsClassName: 'bg-primary text-primary-foreground',20 },21];2223export function WithFallback() {24 return (25 <div className="border-border bg-card w-full max-w-sm rounded-xl border p-4">26 <p className="text-muted-foreground mb-3 text-xs">Pending invites</p>27 <div className="flex flex-col gap-4">28 {invites.map((invite) => (29 <div key={invite.email} className="flex items-center justify-between gap-3">30 <div className="flex min-w-0 items-center gap-3">31 <Avatar>32 {invite.src ? <Avatar.Image src={invite.src} alt={invite.name} /> : null}33 <Avatar.Fallback className={invite.initialsClassName}>34 {invite.initials}35 </Avatar.Fallback>36 </Avatar>37 <div className="min-w-0">38 <p className="truncate text-sm font-medium">{invite.name}</p>39 <p className="text-muted-foreground truncate text-xs">{invite.email}</p>40 </div>41 </div>42 <Badge variant="secondary">Invited</Badge>43 </div>44 ))}45 </div>46 </div>47 );48}
Avatar Group
Display multiple avatars overlapping using
Avatar.Group.Fix cart totals on partial refunds
#412 opened 3 days ago by lucia
IF
NV
MZ+31'use client';23import { Avatar } from '@/components/ui/avatar';45const reviewers = [6 { src: 'https://github.com/figueroaignacio.png', name: 'Ignacio Figueroa', initials: 'IF' },7 { src: 'https://github.com/nicvazquezdev.png', name: 'Nicolas Vazquez', initials: 'NV' },8 { src: 'https://github.com/ManuZarraga.png', name: 'Manuel Zarraga', initials: 'MZ' },9];1011export function AvatarGroup() {12 return (13 <div className="border-border bg-card w-full max-w-sm rounded-xl border p-4">14 <p className="text-sm font-medium">Fix cart totals on partial refunds</p>15 <p className="text-muted-foreground mt-1 text-xs">#412 opened 3 days ago by lucia</p>16 <div className="mt-4 flex items-center gap-3">17 <Avatar.Group>18 {reviewers.map((reviewer) => (19 <Avatar key={reviewer.name} size="sm">20 <Avatar.Image src={reviewer.src} alt={reviewer.name} />21 <Avatar.Fallback>{reviewer.initials}</Avatar.Fallback>22 </Avatar>23 ))}24 <Avatar size="sm">25 <Avatar.Fallback className="bg-muted text-muted-foreground text-xs">+3</Avatar.Fallback>26 </Avatar>27 </Avatar.Group>28 <span className="text-muted-foreground text-xs">6 reviewers requested</span>29 </div>30 </div>31 );32}
API Reference
Avatar
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | Visual size of avatar |
className | string | - | Additional CSS classes |
Found something to improve?
Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.