Avatar
Avatars represent a user or entity using an image, initials fallback, or status indicator. Built on top of Radix UI primitive with support for interactive press states and standardized neutral dark/light themes.
Avatar component featuring image fallbacks, pressable interactions, status dots, and clean dark/light neutral colors.
"use client";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
import { cn } from "@/lib/utils";
import { designRadius } from "@/lib/design-system";
type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
type AvatarColor =
| "default"
| "primary"
| "secondary"
| "accent"
| "success"
| "warning"
| "danger";
type StatusPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
interface AvatarContextValue {
color: AvatarColor;
}
const AvatarContext = React.createContext<AvatarContextValue>({
color: "default",
});
const useAvatarContext = () => React.useContext(AvatarContext);
export interface AvatarProps
extends React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> {
size?: AvatarSize;
color?: AvatarColor;
radius?: keyof typeof designRadius;
isBordered?: boolean;
isDisabled?: boolean;
isPressable?: boolean;
status?: AvatarColor;
statusPosition?: StatusPosition;
}
const avatarSizes: Record<AvatarSize, string> = {
xs: "size-6 text-xs",
sm: "size-8 text-xs",
md: "size-10 text-sm",
lg: "size-12 text-base",
xl: "size-14 text-lg",
"2xl": "size-16 text-xl",
"3xl": "size-20 text-2xl",
};
const avatarColorBorders: Record<AvatarColor, string> = {
default: "ring-2 ring-zinc-300 dark:ring-zinc-700",
primary: "ring-2 ring-primary",
secondary: "ring-2 ring-secondary",
accent: "ring-2 ring-accent",
success: "ring-2 ring-success",
warning: "ring-2 ring-warning",
danger: "ring-2 ring-danger",
};
const statusColors: Record<AvatarColor, string> = {
default: "bg-zinc-400 dark:bg-zinc-500",
primary: "bg-primary",
secondary: "bg-secondary",
accent: "bg-accent",
success: "bg-success",
warning: "bg-warning",
danger: "bg-danger",
};
const statusPositions: Record<StatusPosition, string> = {
"top-left": "top-0 left-0 -translate-x-1/3 -translate-y-1/3",
"top-right": "top-0 right-0 translate-x-1/3 -translate-y-1/3",
"bottom-left": "bottom-0 left-0 -translate-x-1/3 translate-y-1/3",
"bottom-right": "bottom-0 right-0 translate-x-1/3 translate-y-1/3",
};
const fallbackColorMap: Record<AvatarColor, string> = {
default: "bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300",
primary: "bg-sky-500/15 text-sky-600 dark:text-sky-400",
secondary: "bg-purple-500/15 text-purple-600 dark:text-purple-400",
accent: "bg-pink-500/15 text-pink-600 dark:text-pink-400",
success: "bg-emerald-500/15 text-emerald-600 dark:text-emerald-400",
warning: "bg-amber-500/15 text-amber-600 dark:text-amber-400",
danger: "bg-rose-500/15 text-rose-600 dark:text-rose-400",
};
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
AvatarProps
>(
(
{
size = "md",
color = "default",
radius = "full",
isBordered = false,
isDisabled = false,
isPressable = false,
status,
statusPosition = "bottom-right",
className,
children,
tabIndex,
...props
},
ref
) => {
const isEffectivelyDisabled = isDisabled;
return (
<AvatarContext.Provider value={{ color }}>
<div className="relative inline-flex shrink-0">
<AvatarPrimitive.Root
ref={ref}
tabIndex={isPressable && !isEffectivelyDisabled ? tabIndex ?? 0 : tabIndex}
className={cn(
"relative flex shrink-0 overflow-hidden items-center justify-center select-none font-semibold transition-all duration-200",
avatarSizes[size],
designRadius[radius],
isBordered && cn("ring-offset-2 ring-offset-background", avatarColorBorders[color]),
isPressable &&
!isEffectivelyDisabled &&
"cursor-pointer hover:scale-105 active:scale-95 hover:opacity-90 transition-all duration-200 ease-in-out will-change-transform focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-ring outline-none",
isEffectivelyDisabled && "opacity-50 grayscale cursor-not-allowed pointer-events-none",
className
)}
{...props}
>
{children}
</AvatarPrimitive.Root>
{status && (
<span
aria-hidden="true"
className={cn(
"absolute size-3 rounded-full ring-2 ring-white dark:ring-zinc-900 z-10",
statusColors[status],
statusPositions[statusPosition]
)}
/>
)}
</div>
</AvatarContext.Provider>
);
}
);
Avatar.displayName = "Avatar";
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square size-full object-cover", className)}
{...props}
/>
));
AvatarImage.displayName = "AvatarImage";
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => {
const { color } = useAvatarContext();
return (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex size-full items-center justify-center font-semibold text-xs leading-none select-none",
fallbackColorMap[color],
className
)}
{...props}
/>
);
});
AvatarFallback.displayName = "AvatarFallback";
export { Avatar, AvatarImage, AvatarFallback };
export type { AvatarSize, AvatarColor, StatusPosition };
Default
A standard avatar component displaying a user image with an automated initials fallback when the image is absent or loading.
Pressable Avatars (isPressable)
Enable interactive press behavior using isPressable for profile triggers, user menus, or clickable list avatars.
Colors & Bordered Rings
Pair isBordered with any design system color to highlight user status, active stories, or primary roles. Fallbacks automatically adapt soft accent colors.
default | primary | secondary | accent | success | warning | dangerSizes
Scales seamlessly from xs (24px) to 3xl (80px) across predefined design scale tokens.
xs | sm | md | lg | xl | 2xl | 3xlBorder Radius
Controls corner rounding from sharp none to fully circular full.
none | xs | sm | md | lg | xl | 2xl | 3xl | fullStatus Indicators
Adds a status dot indicator (online, away, offline, dnd) positioned at any corner.
top-left | top-right | bottom-left | bottom-rightDisabled State
Applies grayscale filter, reduced opacity, and disables pointer interactions when isDisabled is true.
Props — Avatar
Properties for configuring the Avatar root component.
| Prop | Type | Default | Description |
|---|---|---|---|
| size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'md' | Sets the dimension scale of the avatar. |
| color | 'default' | 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'danger' | 'default' | Theme color for the outer ring when isBordered is true, and for the fallback background. |
| radius | 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full' | 'full' | Corner rounding scale for the avatar element. |
| isBordered | boolean | false | Enables an outer color ring around the avatar frame. |
| isPressable | boolean | false | Enables interactive scale animation and keyboard focus for clickable avatars. |
| isDisabled | boolean | false | Disables interaction and applies opacity + grayscale filter. |
| status | AvatarColor | — | Displays a status dot indicator over the avatar. |
| statusPosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'bottom-right' | Corner alignment position for the status dot. |