CN NT OM ?
import { Avatar } from "@/components/ui/avatar" ;
export default function AvatarDemo () {
return (
< div className = "flex items-center gap-4" >
< Avatar
size = "lg"
src = "https://github.com/shadcn.png"
alt = "shadcn"
fallback = "CN"
status = "online"
/>
< Avatar
size = "lg"
colorful
alt = "Nischal Timalsina"
fallback = "NT"
status = "busy"
/>
< Avatar
size = "lg"
colorful
alt = "Olivia Martin"
fallback = "OM"
status = "away"
/>
< Avatar size = "lg" fallback = "?" status = "offline" />
</ div >
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/avatar
The CLI installs dependencies and any other components this one uses.
import { Avatar, AvatarGroup } from "@/components/ui/avatar" ;
< Avatar src = {user.image} alt = {user.name} fallback = "OM" />
One component, no sub-parts
Unlike shadcn's Avatar / AvatarImage / AvatarFallback trio, this avatar
takes src, alt and fallback as props. It tracks the image's loading
state itself and shows the fallback until the image has loaded, or for good
if it fails.
Avatar: to identify a person, team or workspace next to their name or content.
Badge : for a status label; use the avatar's status prop for presence.
Item : a list row with an avatar as its media, a name and a secondary line.
Use shape="square" for organizations, workspaces and apps, and circles for people, so the two are easy to tell apart.
< AvatarGroup max = { 4 }> { /* optional stack with a "+N" overflow */ }
< Avatar { /* root span: size, shape */ }
src = "…" { /* <img>, shown once loaded */ }
fallback = "OM" { /* initials or icon while loading / on error */ }
status = "online" { /* presence dot in the corner */ }
/>
</ AvatarGroup >
Part Renders Notes Avatar<span>Root. Sized by size, carries data-slot="avatar". Image <img>Rendered when src is set. Hidden (but still loading) until it fires load; removed if it fires error. Fallback <span>The fallback content, shown while the image loads or after it fails. Tinted when colorful is set. Status <span role="img">A dot sized to 28% of the avatar, labelled with the status name. AvatarGroup<div>Overlaps its children and adds a ring around each. With max, renders a "+N" avatar for the rest.
md (32px) is the default. xs and sm fit inline text, table rows and dense lists; lg and xl suit headers and profile cards. Fallback text scales with the size.
OM xs · 20px
OM sm · 24px
OM md · 32px
OM lg · 40px
OM xl · 56px
import { Avatar } from "@/components/ui/avatar" ;
const sizes = [
{ size: "xs" , px: "20px" },
{ size: "sm" , px: "24px" },
{ size: "md" , px: "32px" },
{ size: "lg" , px: "40px" },
{ size: "xl" , px: "56px" },
] as const ;
export default function AvatarSizes () {
return (
< div className = "flex items-end gap-4" >
{sizes. map (( s ) => (
< div key = {s.size} className = "flex flex-col items-center gap-2" >
< Avatar size = {s.size} colorful alt = "Olivia Martin" fallback = "OM" />
< span className = "text-muted-foreground text-xs" >
{s.size} · {s.px}
</ span >
</ div >
))}
</ div >
);
}
circle (default) for people, square for organizations, workspaces and apps.
import { Avatar } from "@/components/ui/avatar" ;
export default function AvatarShapes () {
return (
< div className = "flex items-center gap-6" >
< div className = "flex items-center gap-2" >
< Avatar size = "lg" colorful alt = "Jackson Lee" fallback = "JL" />
< span className = "text-sm" >Person</ span >
</ div >
< div className = "flex items-center gap-2" >
< Avatar
size = "lg"
shape = "square"
colorful
alt = "Northwind Analytics"
fallback = "NA"
/>
< span className = "text-sm" >Workspace</ span >
</ div >
</ div >
);
}
The fallback shows while the image loads, and stays if it fails to load or there's no src. Pass initials, or any node such as an icon for guests. Always pass alt with the person's name: it becomes the image's alt text, and the fallback's accessible name when no image is shown.
CN BI SD
import { UserIcon } from "lucide-react" ;
import { Avatar } from "@/components/ui/avatar" ;
export default function AvatarFallback () {
return (
< div className = "flex items-center gap-4" >
< Avatar
size = "lg"
src = "https://github.com/shadcn.png"
alt = "shadcn"
fallback = "CN"
/>
< Avatar
size = "lg"
src = "https://example.invalid/missing.png"
alt = "Broken image"
fallback = "BI"
/>
< Avatar size = "lg" alt = "Sofia Davis" fallback = "SD" />
< Avatar
size = "lg"
alt = "Guest"
fallback = {< UserIcon className = "size-5" aria-hidden />}
/>
</ div >
);
}
colorful tints the fallback with one of six soft palettes, picked from a hash of alt (or the fallback text when alt is empty). The same name always gets the same color, so people are recognizable across the app, in light and dark mode.
import { Avatar } from "@/components/ui/avatar" ;
const people = [
"Olivia Martin" ,
"Jackson Lee" ,
"Isabella Nguyen" ,
"William Kim" ,
"Sofia Davis" ,
"Lucas Brown" ,
"Amara Okafor" ,
"Kenji Tanaka" ,
];
const initials = ( name : string ) =>
name
. split ( " " )
. map (( n ) => n[ 0 ])
. join ( "" );
export default function AvatarColorful () {
return (
< div className = "flex flex-wrap items-center gap-3" >
{people. map (( name ) => (
< Avatar
key = {name}
size = "lg"
colorful
alt = {name}
fallback = { initials (name)}
/>
))}
</ div >
);
}
status adds a presence dot in the bottom-right corner: online (success), busy (danger), away (warning) or offline (muted). The dot has a background-colored ring so it stays visible on any surface.
OM online
JL busy
IN away
WK offline
import { Avatar } from "@/components/ui/avatar" ;
const people = [
{ name: "Olivia Martin" , initials: "OM" , status: "online" },
{ name: "Jackson Lee" , initials: "JL" , status: "busy" },
{ name: "Isabella Nguyen" , initials: "IN" , status: "away" },
{ name: "William Kim" , initials: "WK" , status: "offline" },
] as const ;
export default function AvatarStatus () {
return (
< div className = "flex flex-wrap items-center gap-6" >
{people. map (( p ) => (
< div key = {p.name} className = "flex flex-col items-center gap-2" >
< Avatar
size = "lg"
colorful
alt = {p.name}
fallback = {p.initials}
status = {p.status}
/>
< span className = "text-muted-foreground text-xs capitalize" >
{p.status}
</ span >
</ div >
))}
</ div >
);
}
Next to a visible name, pass alt="" so the name isn't read twice; the image (or fallback) is then treated as decorative.
CN shadcn m@example.com
IN Isabella Nguyen commented 5m ago
import { Avatar } from "@/components/ui/avatar" ;
export default function AvatarWithText () {
return (
< div className = "flex flex-col gap-4" >
< div className = "flex items-center gap-3" >
< Avatar
src = "https://github.com/shadcn.png"
alt = ""
fallback = "CN"
status = "online"
/>
< div className = "flex flex-col" >
< span className = "font-medium text-sm" >shadcn</ span >
< span className = "text-muted-foreground text-xs" >m@example.com</ span >
</ div >
</ div >
< div className = "flex items-center gap-2 text-sm" >
< Avatar size = "xs" colorful alt = "" fallback = "IN" />
< span >
< span className = "font-medium" >Isabella Nguyen</ span >{ " " }
< span className = "text-muted-foreground" >commented 5m ago</ span >
</ span >
</ div >
</ div >
);
}
AvatarGroup overlaps avatars and rings each one with the background color. max shows that many avatars and collapses the rest into a "+N" avatar. Its size prop sizes the "+N" avatar and every child that doesn't set its own size, so you set it once on the group. A child with an explicit size keeps it. Tighten the overlap with a -space-x-* class.
import { Avatar, AvatarGroup } from "@/components/ui/avatar" ;
const people = [
"Olivia Martin" ,
"Jackson Lee" ,
"Isabella Nguyen" ,
"William Kim" ,
"Sofia Davis" ,
"Lucas Brown" ,
];
const initials = ( name : string ) =>
name
. split ( " " )
. map (( n ) => n[ 0 ])
. join ( "" );
export default function AvatarGroupDemo () {
return (
< div className = "flex flex-col items-start gap-6" >
< AvatarGroup max = { 4 } role = "group" aria-label = "6 collaborators" >
{people. map (( name ) => (
< Avatar key = {name} colorful alt = {name} fallback = { initials (name)} />
))}
</ AvatarGroup >
< AvatarGroup
max = { 3 }
size = "sm"
className = "-space-x-1.5"
role = "group"
aria-label = "6 reviewers"
>
{people. map (( name ) => (
< Avatar key = {name} colorful alt = {name} fallback = { initials (name)} />
))}
</ AvatarGroup >
</ div >
);
}
An avatar as the trigger of a Menu . Wrap it in a React Aria Button with an aria-label, since the avatar alone isn't focusable.
OM
import {
CreditCardIcon,
LogOutIcon,
SettingsIcon,
UserIcon,
} from "lucide-react" ;
import { Button } from "react-aria-components" ;
import { Avatar } from "@/components/ui/avatar" ;
import {
MenuContent,
MenuItem,
MenuSection,
MenuSeparator,
MenuTrigger,
} from "@/components/ui/menu" ;
export default function AvatarRecipeAccountMenu () {
return (
< MenuTrigger >
< Button
aria-label = "Account menu for Olivia Martin"
className = "rounded-full outline-none data-focus-visible:ring-[3px] data-focus-visible:ring-ring/25"
>
< Avatar colorful alt = "" fallback = "OM" status = "online" />
</ Button >
< MenuContent placement = "bottom end" className = "w-56" >
< MenuSection >
< MenuItem textValue = "Profile" href = "#profile" >
< UserIcon /> Profile
</ MenuItem >
< MenuItem textValue = "Billing" href = "#billing" >
< CreditCardIcon /> Billing
</ MenuItem >
< MenuItem textValue = "Settings" href = "#settings" >
< SettingsIcon /> Settings
</ MenuItem >
</ MenuSection >
< MenuSeparator />
< MenuItem textValue = "Log out" variant = "destructive" >
< LogOutIcon /> Log out
</ MenuItem >
</ MenuContent >
</ MenuTrigger >
);
}
A list of comments with avatars aligned to the first line of each message.
IN Isabella Nguyen 2h ago
The new onboarding checklist tested well. Five of six participants finished setup without help.
WK William Kim 1h ago
Nice. Can we ship it behind a flag on Thursday and watch activation for a week?
OM Olivia Martin 12m ago
Flag is ready. I'll turn it on for 20% of new workspaces.
import { Avatar } from "@/components/ui/avatar" ;
const comments = [
{
author: "Isabella Nguyen" ,
initials: "IN" ,
time: "2h ago" ,
body: "The new onboarding checklist tested well. Five of six participants finished setup without help." ,
},
{
author: "William Kim" ,
initials: "WK" ,
time: "1h ago" ,
body: "Nice. Can we ship it behind a flag on Thursday and watch activation for a week?" ,
},
{
author: "Olivia Martin" ,
initials: "OM" ,
time: "12m ago" ,
body: "Flag is ready. I'll turn it on for 20% of new workspaces." ,
},
];
export default function AvatarRecipeComments () {
return (
< ol className = "flex w-full max-w-md flex-col gap-5" >
{comments. map (( c ) => (
< li key = {c.time} className = "flex gap-3" >
< Avatar colorful alt = "" fallback = {c.initials} />
< div className = "flex min-w-0 flex-col gap-1" >
< p className = "text-sm" >
< span className = "font-medium" >{c.author}</ span >{ " " }
< span className = "text-muted-foreground text-xs" >{c.time}</ span >
</ p >
< p className = "text-muted-foreground text-sm leading-relaxed" >
{c.body}
</ p >
</ div >
</ li >
))}
</ ol >
);
}
A square workspace avatar in a Card header and a small AvatarGroup of the team in the footer.
import { Avatar, AvatarGroup } from "@/components/ui/avatar" ;
import { Badge } from "@/components/ui/badge" ;
import {
Card,
CardAction,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card" ;
const team = [
{ name: "Olivia Martin" , initials: "OM" },
{ name: "Jackson Lee" , initials: "JL" },
{ name: "Isabella Nguyen" , initials: "IN" },
{ name: "William Kim" , initials: "WK" },
{ name: "Sofia Davis" , initials: "SD" },
{ name: "Lucas Brown" , initials: "LB" },
{ name: "Amara Okafor" , initials: "AO" },
];
export default function AvatarRecipeProjectCard () {
return (
< Card className = "w-full max-w-sm" >
< CardHeader >
< div className = "mb-2" >
< Avatar
shape = "square"
size = "lg"
colorful
alt = "Atlas logo"
fallback = "AT"
/>
</ div >
< CardTitle >Atlas redesign</ CardTitle >
< CardDescription >
New navigation and dashboard for the Q4 launch.
</ CardDescription >
< CardAction >
< Badge size = "sm" variant = "dot" color = "success" >
On track
</ Badge >
</ CardAction >
</ CardHeader >
< CardFooter className = "justify-between" >
< AvatarGroup
max = { 4 }
size = "sm"
role = "group"
aria-label = { `${ team . length } team members` }
>
{team. map (( p ) => (
< Avatar key = {p.name} colorful alt = {p.name} fallback = {p.initials} />
))}
</ AvatarGroup >
< span className = "text-muted-foreground text-xs" >Due Nov 14</ span >
</ CardFooter >
</ Card >
);
}
With an image, alt is the image's alt text. When no image is shown and alt is set, the fallback gets role="img" and aria-label={alt}.
While an image is still loading, the fallback is aria-hidden and the (invisible) <img> carries the name.
With alt="" (the default), the avatar is decorative: the image has empty alt text and the fallback is aria-hidden. Use this when the name is already visible next to the avatar.
The status dot is role="img" with aria-label set to the status name ("online", "busy", "away", "offline"), so it's announced as a separate image. If presence is shown elsewhere in text, you can omit status.
The "+N" avatar in a group has aria-label="N more". Give the group itself role="group" and an aria-label such as "6 collaborators" for context.
Avatars aren't interactive. To make one clickable, wrap it in a button or link with an accessible name, as in the account menu recipe.
Attribute On Present when data-slot="avatar"Avatar rootAlways data-slot="avatar-group"AvatarGroupAlways
AvatarGroup styles its children through *:data-[slot=avatar]:… (ring and full rounding), so anything you render inside with data-slot="avatar" joins the stack.
className on Avatar goes to the root span; use it for custom sizes (size-12 text-base) or rings.
The ring color in groups is ring-background. Override it on a colored surface with *:data-[slot=avatar]:ring-card on the group.
colorful palettes are defined in the component source as OKLCH values with dark-mode pairs. Edit that array to match your brand.
Also accepts every prop of <span>.
Also accepts every prop of <div>.
Item : list rows with an avatar as media.
Badge : labels next to a name, like roles or "Verified".
Menu : account menus triggered by an avatar.
Card : profile and project cards.