Avatar
User photos with an initials fallback, status badges and stacked groups that collapse into a count.
pnpm dlx shadcn@latest add https://hextaui.com/r/avatar.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cnCopy and paste the following code into your project.
components/ui/avatar.tsx Update the import paths to match your project setup.
Sizes and shapes
Five sizes, as circles or squares. Initials and the user icon scale with the box, and square corners step down with the size. An empty <AvatarFallback /> shows the user icon.
Loading
Initials show while the photo loads, then the photo fades in over them. A broken photo keeps the fallback. Pass delay to wait before showing initials, so fast photos never flash them.
Initials
getInitials() picks the first and last initial. It handles email addresses, emoji, CJK and RTL names, combining marks, and names with no letters at all.
- ALAda Lovelace
- MMadonna
- JPjean-luc picard
- AL[email protected]
- AJ(Admin) John
- 👩👩👧👦F👩👩👧👦 Family
- 山太山田 太郎
- معمحمد علي
- ZTZ̷̢̛͖͓̰̈́algo T̵ext
- MCMary Ann Evans Cross
- !!! ???
- (empty)
Status
<AvatarBadge /> sits on the rim at every size and shape. Set status for a colored dot with an accessible label, or pass an icon. Changing the status plays a single pulse.
Group
<AvatarGroup /> overlaps its avatars and sets their size and shape. max collapses the rest into a count.
Linked group
Render avatars as links with render and give each an aria-label. A focused avatar rises above its neighbours so the ring is never cut. Add <AvatarGroupCount /> yourself when the total comes from your data.
Layout
Avatars never shrink in tight rows. A size class like size-20 scales the initials and badge with it, and long initials never overflow.
Right to left
The badge stays on the end corner, which is the left in RTL, and groups overlap from the right.
Avatars aren’t focusable on their own. Rendered as a link or button they get the usual keys.
| Key | Action |
|---|---|
| Tab | Moves focus to the next linked avatar. |
| Enter | Follows the focused link. |
- Use
alt=""when the person’s name is already next to the avatar, and their name as the alt text when it isn’t. - Badges with a
statusare announced as “Online”, “Away”, “Busy” or “Offline”. Offline is drawn as a ring, so the status never relies on color alone. - Groups have
role="group". The count reads as “3 more”, not “+3”. - With reduced motion on, photos appear without fading and status changes don’t pulse.
Built on the Base UI avatar. Every part accepts the attributes of the element it renders. The styles are exported as avatarVariants and avatarBadgeVariants.
| Prop | Type | Default |
|---|---|---|
sizeInherited from the group when omitted. | "xs" | "sm" | "default" | "lg" | "xl" | "default" |
shapeInherited from the group when omitted. | "circle" | "square" | "circle" |
render | ReactElement | (props, state) => ReactElement | <span> |
| Attribute | Description |
|---|---|
data-slot="avatar" | Target avatars in CSS. |
data-size | The resolved size. |
data-shape | The resolved shape. |
--avatar-radius | The corner radius, shared by every layer. |
| Prop | Type | Default |
|---|---|---|
src | string | – |
alt | string | – |
onLoadingStatusChange | (status: "idle" | "loading" | "loaded" | "error") => void | – |
keepMountedLoad the image in place instead of preloading it, for loading="lazy" or next/image. | boolean | false |
render | ReactElement | (props, state) => ReactElement | <img> |
| Attribute | Description |
|---|---|
data-slot="avatar-image" | Target images in CSS. |
data-loading | Present while the image loads. |
data-error | Present when the image failed to load. |
data-starting-style | Present while the image fades in. |
data-ending-style | Present while the image fades out. |
| Prop | Type | Default |
|---|---|---|
childrenEmpty or whitespace shows the user icon. | ReactNode | <IconUser /> |
delayMilliseconds to wait before showing it. | number | 0 |
render | ReactElement | (props, state) => ReactElement | <span> |
| Attribute | Description |
|---|---|
data-slot="avatar-fallback" | Target fallbacks in CSS. |
data-ready | false until the delay has passed. |
| Prop | Type | Default |
|---|---|---|
statusColors the dot and labels it for assistive tech. Without it the badge uses the primary color. | "online" | "away" | "busy" | "offline" | – |
childrenAn icon inside the badge. Hidden at the xs and sm sizes. | ReactNode | – |
| Attribute | Description |
|---|---|
data-slot="avatar-badge" | Target badges in CSS. |
data-status | The current status. |
data-slot="avatar-badge-pulse" | The pulse played after a status change. |
| Prop | Type | Default |
|---|---|---|
size | "xs" | "sm" | "default" | "lg" | "xl" | "default" |
shape | "circle" | "square" | "circle" |
maxHow many items to show, including the count. Values below 2 are raised to 2. | number | – |
| Attribute | Description |
|---|---|
data-slot="avatar-group" | Target groups in CSS. |
data-size | The group’s size. |
| Prop | Type | Default |
|---|---|---|
countShown as +3, or 99+ above 99. | number | – |
childrenReplaces the count, for example with an icon. | ReactNode | – |
sizeInherited from the group when omitted. | "xs" | "sm" | "default" | "lg" | "xl" | – |
shapeInherited from the group when omitted. | "circle" | "square" | – |
| Attribute | Description |
|---|---|
data-slot="avatar-group-count" | Target the count in CSS. |
data-size | The resolved size. |
data-shape | The resolved shape. |
getInitials(name, max = 2) returns up to max uppercase initials: the first word’s and the last word’s. For an email address it uses the part before the @. It returns an empty string when the name has no letters, numbers or emoji, so the fallback shows the user icon.