Avatar
An identity avatar — circle / square / hexagon, with presence & status indicators, a name + detail AvatarItem, and overlapping AvatarGroups.
@garn/ui/avatarOn this page
Default
Image with an initials fallback.
Sizing
Fixed identity sizes — 16 / 24 / 32 / 40 / 48 / 96 / 128px. Intentionally not affected by density.
Shapes
circle · square · hexagon.
Fallback
Initials, or the default person glyph when empty.
Presence
online · busy · focus · offline (bottom-end, cutout ring).
Status
approved · declined · locked (top-end).
Interactive
asChild link with hover/press/focus + a presence dot.
Item
AvatarItem — name + secondary line; static and interactive/selected.
Group
AvatarGroup stack with a '+N' overflow popover.
Grid Group
AvatarGroup grid appearance + `total` (surplus tallied into '+N').
Skeleton
AvatarSkeleton loading placeholders (circle / square / hexagon).
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add avatar- Registry
avatar- Deps
@radix-ui/react-avatar@radix-ui/react-slotclass-variance-authoritylucide-react- Registry deps
utilspopoverscroll-area
Import
import { Avatar, AvatarImage, AvatarFallback, AvatarSkeleton, AvatarPresence, AvatarStatus, AvatarItem, AvatarGroup } from "@garn/ui/avatar";Anatomy
The parts this component is built from — compose them to assemble it.
AvatarRequired[data-slot="avatar"]The frame; carries the size + shape variants. Not overflow-hidden, so indicators can sit past the edge. asChild makes it an interactive <a>/<button>.
AvatarImage[data-slot="avatar-image"]The photo; needs alt. Rounds/clips itself to the frame shape. Hidden until it loads, then revealed over the fallback.
AvatarFallbackRequired[data-slot="avatar-fallback"]Initials shown while the image loads or if it fails. With no children it renders a neutral person glyph — never a blank disc.
AvatarSkeleton[data-slot="avatar-skeleton"]A pulsing loading placeholder matching the avatar's size + shape (hexagon self-clips). Reduced-motion-safe.
AvatarPresence[data-slot="avatar-presence"]A presence dot anchored bottom-end (online / busy / focus / offline) with a surface-color cutout ring and an sr-only state label.
AvatarStatus[data-slot="avatar-status"]A status glyph anchored top-end (approved / declined / locked) with a surface-color cutout ring and an sr-only state label.
AvatarItem[data-slot="avatar-item"]An avatar + primaryText (+ optional secondaryText) row for lists and menus. asChild promotes the whole row to a button/link; supports selected / disabled.
AvatarGroup[data-slot="avatar-group"]A labelled list of avatars — stack (overlapping, surface-ring separated) or grid (even gutters) — capped at max, with a '+N' overflow chip.
Props
The component's public props and their types.
shape- Description
- No description.
- Type
"circle" | "hexagon" | "square"- Default
circle
size- Description
- No description.
- Type
"2xl" | "2xs" | "lg" | "md" | "sm" | "xl" | "xs"- Default
md
asChild- Description
- Render the consumer's element (an `<a>`/`<button>`) as the root — makes the avatar interactive (hover tint + focus ring).
- Type
boolean- Default
false
Plus 280 inherited native <span> attributes.
AvatarSkeleton
shape- Description
- Match the avatar it stands in for.
- Type
AvatarShape- Default
"circle"
size- Description
- Match the avatar it stands in for.
- Type
AvatarSize- Default
"md"
Plus 280 inherited native HTML attributes.
AvatarPresence
label- Description
- Accessible label; defaults to the capitalized state word.
- Type
string
typeRequired- Description
- Which presence state to show.
- Type
AvatarPresenceType
Plus 279 inherited native HTML attributes.
AvatarStatus
label- Description
- Accessible label; defaults to the capitalized state word.
- Type
string
typeRequired- Description
- Which status to show.
- Type
AvatarStatusType
Plus 279 inherited native HTML attributes.
AvatarItem
asChild- Description
- Promote the whole row to the consumer's interactive element (a `<button>`/`<a>`).
- Type
boolean- Default
false
avatar- Description
- The leading Avatar.
- Type
React.ReactNode
primaryTextRequired- Description
- The primary line — usually the person's name.
- Type
React.ReactNode
secondaryText- Description
- An optional muted second line under the primary.
- Type
React.ReactNode
selected- Description
- Reflected as `data-selected` and styled as the active row.
- Type
boolean
Plus 280 inherited native HTML attributes.
AvatarGroup
appearance- Description
- `stack` overlaps the avatars; `grid` lays them out with even gutters.
- Type
"grid" | "stack"- Default
"stack"
label- Description
- Accessible name for the group (required — the group is a labelled list).
- Type
string
max- Description
- Show at most this many avatars before collapsing the rest into "+N". Defaults to 5 (stack) / 11 (grid).
- Type
number
moreLabel- Description
- Accessible label for the "+N" chip (default: "N more").
- Type
string
onAvatarClick- Description
- Fires with the avatar's index when a stacked avatar is clicked.
- Type
(index: number) => void
onMoreClick- Description
- Fires when the "+N" chip is clicked. When set, it replaces the default popover.
- Type
() => void
shape- Description
- Shape applied to every avatar + the chip.
- Type
AvatarShape- Default
"circle"
size- Description
- Size applied to every avatar + the chip.
- Type
AvatarSize- Default
"sm"
total- Description
- Total people represented when not all are passed as children — the surplus is added to "+N".
- Type
number
Plus 279 inherited native HTML attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="avatar"]States
| Selector | State | Description |
|---|---|---|
[data-slot=avatar-image] | image-loaded | Image resolved; it covers the fallback. |
[data-slot=avatar-fallback] | fallback-shown | No (or not-yet-loaded) image — the fallback (initials / glyph) is visible. |
[data-interactive] | interactive | asChild avatar: hover tint overlay, active press-scale (0.9), and a focus-visible ring. |
[data-selected] | selected | AvatarItem active/selected row (bg-accent). |
| Property | Token | Tier |
|---|---|---|
presence-online | --garn-avatar-onlinebrandable | component |
presence-busy | --garn-avatar-busybrandable | component |
presence-focus | --garn-avatar-focusbrandable | component |
presence-offline | --garn-avatar-offlinebrandable | component |
status-locked | --garn-avatar-lockedbrandable | component |
Private instance vars (never externalize): --avatar-hex
When to use
Represent a person or entity with their photo, falling back to initials or a person glyph — with optional presence/status, a name + detail AvatarItem, or an AvatarGroup that stacks many.
Reach for something else when
- A decorative or content image (use img / aspect-ratio)
- a standalone status dot or unread count (use a badge / counter)
- a large hero image.
Overview
Avatar represents a person or entity with their photo, gracefully falling back to initials or a glyph when the image is missing or still loading. AvatarImage + AvatarFallback are the core; AvatarStatus/AvatarPresence add a status dot, AvatarSkeleton holds the shape while loading, and an AvatarGroup stacks several with a "+N" overflow. It's an identity marker — not a button — so pair it with a name and wrap it in a control when it should be clickable.
Guidelines
Always provide a real fallback. Photos fail, lag, or simply don't exist; give every avatar initials (or a glyph) so it never renders empty. Derive initials from the person's name and keep them to one or two characters.
Give it an accessible name, not just a picture. Set meaningful alt/label text (the person's name) — a decorative-only avatar leaves screen-reader users with nothing. In a group, summarize the overflow ("+5 more") rather than hiding who's there.
Use the status dot for presence, sparingly. A green/idle/offline dot communicates availability at a glance, but don't overload the corner with multiple indicators. Keep sizes on the avatar's own scale and pair small avatars with a name when identity matters.
Best practices
- Always include an AvatarFallback (initials or the default glyph) for the loading/missing-image case.
- Give AvatarImage a meaningful
alt; give an interactive (asChild) avatar anaria-label. - Use the fixed
sizescale — avatars are identity geometry, not density-driven. - Use AvatarPresence (bottom-end) for presence and AvatarStatus (top-end) for status; give every AvatarGroup a
label.
- Don't ship an Avatar with no fallback.
- Don't wrap the avatar in an overflow-hidden box if it carries presence/status — that clips the indicator.
- Don't shrink avatars with density; the size scale is intentional.
Content guidelines
- Use 1–2 uppercase initials (first + last) for the fallback; keep fallbacks consistent across a list.
- Write an alt that names the person or entity — not “avatar” or “user image”.
- When an AvatarGroup represents more people than you render, pass
totalso '+N' reflects the real count.
Troubleshooting
The avatar is blank when the image URL is missing or slow.
Cause. No AvatarFallback was provided.
Fix. Always render an AvatarFallback (initials, or leave it empty for the default person glyph) as a child.
A screen reader doesn't announce a clickable avatar.
Cause. An interactive (asChild) avatar has no accessible name on its <a>/<button>.
Fix. Add aria-label (the person's name) to the link/button, or give AvatarImage a meaningful alt.
The presence/status dot is clipped or cut off.
Cause. The avatar (or a wrapping container) was given overflow-hidden — the indicator sits past the frame edge.
Fix. Don't add overflow-hidden to the Avatar or its wrapper; the image/fallback already clip themselves.
AvatarGroup shows fewer avatars than expected, or the '+N' count looks off.
Cause. max reserves the last slot for the '+N' chip; the chip counts everything beyond the shown avatars.
Fix. Raise max, or pass total when the group represents more people than the children you render.
Editing dark-mode presence/status colors in the component has no effect.
Cause. They resolve through the --garn-avatar-* component tokens; the dark values live in the token layer.
Fix. Adjust dark mode in the garn theme CSS (.dark block) — never with raw ramps in avatar.tsx.
Accessibility
Accessibility requirements
Give the interactive avatar an accessible name — aria-label (the person's name) on the <a>/<button>, or a meaningful AvatarImage alt / AvatarFallback text.
when an Avatar is interactive (asChild) but the link/button, the AvatarImage alt, and the AvatarFallback text are all empty
Give the AvatarGroup an accessible name via `label` (e.g. "Project team").
when an AvatarGroup has no `label` or `aria-label`
- AvatarImage is an <img> — give it a meaningful `alt` (the person/entity name), or alt="" if it is purely decorative beside a text label.
- AvatarFallback (initials or the default person glyph) is what assistive tech reads before/if the image loads.
- An interactive avatar (asChild) needs its own accessible name — put aria-label (the person's name) on the <a>/<button>; a meaningful AvatarImage alt or AvatarFallback text also counts. A dev-time warning fires when none is found.
- AvatarPresence / AvatarStatus convey their state in text via an sr-only label (e.g. 'Online', 'Approved') and a distinct glyph shape — never by color alone.
- AvatarGroup is a labelled list (role=list) — give it an accessible name via `label`; a dev-time warning fires when missing. The '+N' chip is a button labelled 'N more'.