Avatar

An image element with a fallback for representing the user.

@behsseB

Installation

Install the component Avatar in your project using the CLI.

Avatar.tsx

pnpm dlx behsseui@latest add Avatar

Install the component manually.

Create a ui folder at the root of the project, then a component folder inside it, and finally a Avatar.tsx file in that folder.

Copy and paste the following code into your project.

ui/components/Avatar.tsx

1"use client"
2
3import type { ReactNode, ImgHTMLAttributes, HTMLAttributes } from "react"
4import { createContext, useContext, useState, Children, isValidElement } from "react"
5import { cva, type VariantProps } from "class-variance-authority"
6import { cn } from "@/lib/utils"
7
8const avatarWrapperVariants = cva(
9 "relative inline-flex shrink-0",
10 {
11 variants: {
12 size: {
13 default: "h-10 w-10",
14 sm: "h-8 w-8",
15 lg: "h-12 w-12",
16 xl: "h-16 w-16",
17 },
18 },
19 defaultVariants: {
20 size: "default",
21 },
22 }
23)
24
25const avatarVariants = cva(
26 "h-full w-full overflow-hidden rounded-full ring-2 ring-background"
27)
28
29const avatarImageVariants = cva("aspect-square h-full w-full object-cover")
30
31const avatarFallbackVariants = cva(
32 "flex h-full w-full items-center justify-center rounded-full bg-muted text-muted-foreground",
33 {
34 variants: {
35 size: {
36 default: "text-sm",
37 sm: "text-xs",
38 lg: "text-base",
39 xl: "text-lg",
40 },
41 },
42 defaultVariants: {
43 size: "default",
44 },
45 }
46)
47
48const avatarBadgeVariants = cva(
49 "absolute flex items-center justify-center rounded-full border-2 border-background",
50 {
51 variants: {
52 size: {
53 default: "h-3 w-3 bottom-0 right-0",
54 sm: "h-2.5 w-2.5 bottom-0 right-0",
55 lg: "h-3.5 w-3.5 bottom-0 right-0",
56 xl: "h-4 w-4 bottom-0.5 right-0.5",
57 },
58 status: {
59 online: "bg-green-500",
60 offline: "bg-gray-400",
61 busy: "bg-red-500",
62 away: "bg-yellow-500",
63 },
64 },
65 defaultVariants: {
66 size: "default",
67 status: "online",
68 },
69 }
70)
71
72type ImageLoadingStatus = "idle" | "loading" | "loaded" | "error"
73
74type AvatarContextType = {
75 status: ImageLoadingStatus
76 setStatus: (status: ImageLoadingStatus) => void
77 size?: "default" | "sm" | "lg" | "xl" | null
78}
79
80const AvatarContext = createContext<AvatarContextType | undefined>(undefined)
81
82function useAvatar() {
83 const context = useContext(AvatarContext)
84 if (!context) {
85 throw new Error("Avatar components must be used within an Avatar")
86 }
87 return context
88}
89
90type AvatarProps = {
91 children: ReactNode
92 className?: string
93} & VariantProps<typeof avatarWrapperVariants>
94
95export function Avatar({ children, className, size = "default" }: AvatarProps) {
96 const [status, setStatus] = useState<ImageLoadingStatus>("idle")
97
98 // Séparer le badge des autres enfants
99 const childArray = Children.toArray(children)
100 const badge = childArray.find(
101 (child) => isValidElement(child) && (child.type as { displayName?: string })?.displayName === "AvatarBadge"
102 )
103 const otherChildren = childArray.filter(
104 (child) => !(isValidElement(child) && (child.type as { displayName?: string })?.displayName === "AvatarBadge")
105 )
106
107 return (
108 <AvatarContext.Provider value={{ status, setStatus, size }}>
109 <span className={cn(avatarWrapperVariants({ size, className }))}>
110 <span className={cn(avatarVariants())}>
111 {otherChildren}
112 </span>
113 {badge}
114 </span>
115 </AvatarContext.Provider>
116 )
117}
118
119type AvatarImageProps = {
120 className?: string
121 src?: string
122 alt?: string
123} & Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">
124
125export function AvatarImage({ className, src, alt = "", ...props }: AvatarImageProps) {
126 const { status, setStatus } = useAvatar()
127
128 if (status === "error" || !src) {
129 return null
130 }
131
132 return (
133 <img
134 src={src}
135 alt={alt}
136 className={cn(avatarImageVariants(), className)}
137 onLoadStart={() => setStatus("loading")}
138 onLoad={() => setStatus("loaded")}
139 onError={() => setStatus("error")}
140 {...props}
141 />
142 )
143}
144
145type AvatarFallbackProps = {
146 children: ReactNode
147 className?: string
148 delayMs?: number
149} & HTMLAttributes<HTMLSpanElement>
150
151export function AvatarFallback({ children, className, delayMs, ...props }: AvatarFallbackProps) {
152 const { status, size } = useAvatar()
153 const [canRender, setCanRender] = useState(delayMs === undefined)
154
155 // Handle delay
156 if (delayMs !== undefined && !canRender) {
157 setTimeout(() => setCanRender(true), delayMs)
158 }
159
160 // Don't show fallback if image loaded successfully
161 if (status === "loaded") {
162 return null
163 }
164
165 // Don't render until delay has passed (if specified)
166 if (!canRender) {
167 return null
168 }
169
170 // Show fallback only when idle (no image) or error
171 if (status !== "idle" && status !== "error") {
172 return null
173 }
174
175 return (
176 <span className={cn(avatarFallbackVariants({ size, className }))} {...props}>
177 {children}
178 </span>
179 )
180}
181
182type AvatarBadgeProps = {
183 children?: ReactNode
184 className?: string
185} & VariantProps<typeof avatarBadgeVariants> & HTMLAttributes<HTMLSpanElement>
186
187export function AvatarBadge({ children, className, status = "online", ...props }: AvatarBadgeProps) {
188 const { size } = useAvatar()
189
190 return (
191 <span className={cn(avatarBadgeVariants({ size, status, className }))} {...props}>
192 {children}
193 </span>
194 )
195}
196
197AvatarBadge.displayName = "AvatarBadge"
198
199// Avatar Group
200
201type AvatarGroupProps = {
202 children: ReactNode
203 className?: string
204 max?: number
205} & HTMLAttributes<HTMLDivElement>
206
207export function AvatarGroup({ children, className, max, ...props }: AvatarGroupProps) {
208 const childArray = Children.toArray(children)
209 const visibleChildren = max ? childArray.slice(0, max) : childArray
210 const remainingCount = max ? childArray.length - max : 0
211
212 return (
213 <div className={cn("flex -space-x-3", className)} {...props}>
214 {visibleChildren}
215 {remainingCount > 0 && (
216 <AvatarGroupCount count={remainingCount} />
217 )}
218 </div>
219 )
220}
221
222type AvatarGroupCountProps = {
223 count: number
224 className?: string
225} & HTMLAttributes<HTMLSpanElement>
226
227export function AvatarGroupCount({ count, className, ...props }: AvatarGroupCountProps) {
228 return (
229 <span
230 className={cn(
231 "relative inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground text-sm font-medium ring-2 ring-background",
232 className
233 )}
234 {...props}
235 >
236 +{count}
237 </span>
238 )
239}
240

Usages

Different variants and use cases for the Avatar component.

Default

An avatar with an image and fallback.

@behsseBH

Default.tsx

<Avatar>
  <AvatarImage src="/behsse-logo.jpg" alt="@behsse" />
  <AvatarFallback>BH</AvatarFallback>
</Avatar>

Fallback

An avatar showing only the fallback (when image fails or is not provided).

BH

Fallback.tsx

<Avatar>
  <AvatarImage src="" alt="@user" />
  <AvatarFallback>BH</AvatarFallback>
</Avatar>

With Badge

An avatar with a status badge indicator.

@behsseBH

With Badge.tsx

<Avatar>
  <AvatarImage src="/behsse-logo.jpg" alt="@behsse" />
  <AvatarFallback>BH</AvatarFallback>
  <AvatarBadge status="online" />
</Avatar>

Badge with Icon

An avatar with a badge containing an icon.

@behsseBH

Badge with Icon.tsx

import Check from "@/ui/icons/Check"

<Avatar>
  <AvatarImage src="/behsse-logo.jpg" alt="@behsse" />
  <AvatarFallback>BH</AvatarFallback>
  <AvatarBadge status="online" className="h-4 w-4 bg-accent-foreground">
    <Check className="h-1.5 w-1.5 fill-accent" />
  </AvatarBadge>
</Avatar>

Avatar Group

Multiple avatars stacked together.

@behsseBHABCD

Avatar Group.tsx

<AvatarGroup>
  <Avatar>
    <AvatarImage src="/behsse-logo.jpg" alt="@behsse" />
    <AvatarFallback>BH</AvatarFallback>
  </Avatar>
  <Avatar>
    <AvatarFallback>AB</AvatarFallback>
  </Avatar>
  <Avatar>
    <AvatarFallback>CD</AvatarFallback>
  </Avatar>
</AvatarGroup>

Avatar Group Count

An avatar group with a maximum count, showing remaining avatars as a number.

@behsseBHABCD+7

Avatar Group Count.tsx

<AvatarGroup max={3}>
  <Avatar>
    <AvatarImage src="/behsse-logo.jpg" alt="@behsse" />
    <AvatarFallback>BH</AvatarFallback>
  </Avatar>
  <Avatar>
    <AvatarFallback>AB</AvatarFallback>
  </Avatar>
  <Avatar>
    <AvatarFallback>CD</AvatarFallback>
  </Avatar>
  <Avatar>
    <AvatarFallback>EF</AvatarFallback>
  </Avatar>
  <Avatar>
    <AvatarFallback>GH</AvatarFallback>
  </Avatar>
  {/* ... more avatars */}
</AvatarGroup>

Sizes

Avatars come in different sizes: sm, default, lg, and xl.

@behsseBH@behsseBH@behsseBH@behsseBH

Sizes.tsx

<div className="flex items-center gap-4">
  <Avatar size="sm">
    <AvatarImage src="/behsse-logo.jpg" alt="@behsse" />
    <AvatarFallback>BH</AvatarFallback>
  </Avatar>
  <Avatar size="default">
    <AvatarImage src="/behsse-logo.jpg" alt="@behsse" />
    <AvatarFallback>BH</AvatarFallback>
  </Avatar>
  <Avatar size="lg">
    <AvatarImage src="/behsse-logo.jpg" alt="@behsse" />
    <AvatarFallback>BH</AvatarFallback>
  </Avatar>
  <Avatar size="xl">
    <AvatarImage src="/behsse-logo.jpg" alt="@behsse" />
    <AvatarFallback>BH</AvatarFallback>
  </Avatar>
</div>