Avatar
An image element with a fallback for representing the user.
BInstallation
Install the component Avatar in your project using the CLI.
Avatar.tsx
pnpm dlx behsseui@latest add AvatarInstall 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"23import 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"78const 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)2425const avatarVariants = cva(26 "h-full w-full overflow-hidden rounded-full ring-2 ring-background"27)2829const avatarImageVariants = cva("aspect-square h-full w-full object-cover")3031const 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)4748const 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)7172type ImageLoadingStatus = "idle" | "loading" | "loaded" | "error"7374type AvatarContextType = {75 status: ImageLoadingStatus76 setStatus: (status: ImageLoadingStatus) => void77 size?: "default" | "sm" | "lg" | "xl" | null78}7980const AvatarContext = createContext<AvatarContextType | undefined>(undefined)8182function useAvatar() {83 const context = useContext(AvatarContext)84 if (!context) {85 throw new Error("Avatar components must be used within an Avatar")86 }87 return context88}8990type AvatarProps = {91 children: ReactNode92 className?: string93} & VariantProps<typeof avatarWrapperVariants>9495export function Avatar({ children, className, size = "default" }: AvatarProps) {96 const [status, setStatus] = useState<ImageLoadingStatus>("idle")9798 // Séparer le badge des autres enfants99 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 )106107 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}118119type AvatarImageProps = {120 className?: string121 src?: string122 alt?: string123} & Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt">124125export function AvatarImage({ className, src, alt = "", ...props }: AvatarImageProps) {126 const { status, setStatus } = useAvatar()127128 if (status === "error" || !src) {129 return null130 }131132 return (133 <img134 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}144145type AvatarFallbackProps = {146 children: ReactNode147 className?: string148 delayMs?: number149} & HTMLAttributes<HTMLSpanElement>150151export function AvatarFallback({ children, className, delayMs, ...props }: AvatarFallbackProps) {152 const { status, size } = useAvatar()153 const [canRender, setCanRender] = useState(delayMs === undefined)154155 // Handle delay156 if (delayMs !== undefined && !canRender) {157 setTimeout(() => setCanRender(true), delayMs)158 }159160 // Don't show fallback if image loaded successfully161 if (status === "loaded") {162 return null163 }164165 // Don't render until delay has passed (if specified)166 if (!canRender) {167 return null168 }169170 // Show fallback only when idle (no image) or error171 if (status !== "idle" && status !== "error") {172 return null173 }174175 return (176 <span className={cn(avatarFallbackVariants({ size, className }))} {...props}>177 {children}178 </span>179 )180}181182type AvatarBadgeProps = {183 children?: ReactNode184 className?: string185} & VariantProps<typeof avatarBadgeVariants> & HTMLAttributes<HTMLSpanElement>186187export function AvatarBadge({ children, className, status = "online", ...props }: AvatarBadgeProps) {188 const { size } = useAvatar()189190 return (191 <span className={cn(avatarBadgeVariants({ size, status, className }))} {...props}>192 {children}193 </span>194 )195}196197AvatarBadge.displayName = "AvatarBadge"198199// Avatar Group200201type AvatarGroupProps = {202 children: ReactNode203 className?: string204 max?: number205} & HTMLAttributes<HTMLDivElement>206207export function AvatarGroup({ children, className, max, ...props }: AvatarGroupProps) {208 const childArray = Children.toArray(children)209 const visibleChildren = max ? childArray.slice(0, max) : childArray210 const remainingCount = max ? childArray.length - max : 0211212 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}221222type AvatarGroupCountProps = {223 count: number224 className?: string225} & HTMLAttributes<HTMLSpanElement>226227export function AvatarGroupCount({ count, className, ...props }: AvatarGroupCountProps) {228 return (229 <span230 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 className233 )}234 {...props}235 >236 +{count}237 </span>238 )239}240Usages
Different variants and use cases for the Avatar component.
Default
An avatar with an image and fallback.
BHDefault.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).
Fallback.tsx
<Avatar>
<AvatarImage src="" alt="@user" />
<AvatarFallback>BH</AvatarFallback>
</Avatar>With Badge
An avatar with a status badge indicator.
BHWith 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.
BHBadge 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.
BHABCDAvatar 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.
BHABCD+7Avatar 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.
BH
BH
BH
BHSizes.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>