HextaUI

Avatar

User photos with an initials fallback, status badges and stacked groups that collapse into a count.

ALGH
ALATGH2 more
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
} from "@/components/ui/avatar"

export function AvatarDemo() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-8">
      <Avatar size="xl">
        <AvatarImage src="/preview/landscape.svg" alt="" />
        <AvatarFallback>AL</AvatarFallback>
        <AvatarBadge status="online" />
      </Avatar>
      <Avatar size="xl" shape="square">
        <AvatarFallback>GH</AvatarFallback>
      </Avatar>
      <AvatarGroup size="lg" max={4}>
        <Avatar>
          <AvatarImage src="/preview/landscape.svg" alt="" />
          <AvatarFallback>AL</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarFallback>AT</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarFallback>GH</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarFallback>KJ</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarFallback>LT</AvatarFallback>
        </Avatar>
      </AvatarGroup>
    </div>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/avatar.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
} from "@/components/ui/avatar"
<Avatar>
  <AvatarImage src="/ada.jpg" alt="" />
  <AvatarFallback>AL</AvatarFallback>
</Avatar>
Avatar
├── AvatarImage
├── AvatarFallback
└── AvatarBadge

AvatarGroup
├── Avatar
└── AvatarGroupCount

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.

ALALALALALLTLTLTLTLT
ALALALALALLTLTLTLTLT
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"

const sizes = ["xs", "sm", "default", "lg", "xl"] as const
const shapes = ["circle", "square"] as const

export function AvatarSizes() {
  return (
    <div className="flex flex-col gap-4">
      {shapes.map((shape) => (
        <div key={shape} className="flex flex-wrap items-center gap-3">
          {sizes.map((size) => (
            <Avatar key={`photo-${size}`} size={size} shape={shape}>
              <AvatarImage src="/preview/landscape.svg" alt="" />
              <AvatarFallback>AL</AvatarFallback>
            </Avatar>
          ))}
          {sizes.map((size) => (
            <Avatar key={`initials-${size}`} size={size} shape={shape}>
              <AvatarFallback>LT</AvatarFallback>
            </Avatar>
          ))}
          {sizes.map((size) => (
            <Avatar key={`empty-${size}`} size={size} shape={shape}>
              <AvatarFallback />
            </Avatar>
          ))}
        </div>
      ))}
    </div>
  )
}

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.

ATBIGH
"use client"

import * as React from "react"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"

export function AvatarLoading() {
  const [version, setVersion] = React.useState(0)
  const [loading, setLoading] = React.useState(false)
  const timer = React.useRef<number>(undefined)

  React.useEffect(() => () => window.clearTimeout(timer.current), [])

  const reload = () => {
    window.clearTimeout(timer.current)
    setLoading(true)
    timer.current = window.setTimeout(() => {
      setVersion((value) => value + 1)
      setLoading(false)
    }, 1200)
  }

  return (
    <div className="flex flex-wrap items-center gap-3">
      <Avatar size="xl">
        {loading ? null : (
          <AvatarImage src={`/preview/landscape.svg?v=${version}`} alt="" />
        )}
        <AvatarFallback>AT</AvatarFallback>
      </Avatar>
      <Avatar size="xl">
        <AvatarImage src="/preview/missing.png" alt="" />
        <AvatarFallback>BI</AvatarFallback>
      </Avatar>
      <Avatar size="xl">
        <AvatarImage src="/preview/landscape.svg" alt="" />
        <AvatarFallback delay={600}>GH</AvatarFallback>
      </Avatar>
      <Button variant="outline" size="sm" onClick={reload}>
        Load a new photo
      </Button>
    </div>
  )
}

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)
"use client"

import { Avatar, AvatarFallback, getInitials } from "@/components/ui/avatar"

const names = [
  "Ada Lovelace",
  "Madonna",
  "jean-luc picard",
  "[email protected]",
  "(Admin) John",
  "👩‍👩‍👧‍👦 Family",
  "山田 太郎",
  "محمد علي",
  "Z̷̢̛͖͓̰̈́algo T̵ext",
  "Mary Ann Evans Cross",
  "!!! ???",
  "",
]

export function AvatarInitials() {
  return (
    <ul className="grid w-full max-w-md grid-cols-1 gap-2 sm:grid-cols-2">
      {names.map((name) => (
        <li key={name} className="flex min-w-0 items-center gap-2 text-sm">
          <Avatar>
            <AvatarFallback>{getInitials(name)}</AvatarFallback>
          </Avatar>
          <span className="min-w-0 truncate text-muted-foreground">
            {name || "(empty)"}
          </span>
        </li>
      ))}
    </ul>
  )
}

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.

GHGHGHGHGHAT
GHGHGHGHGHAT
online
"use client"

import * as React from "react"
import { IconCheck } from "@tabler/icons-react"

import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarImage,
  type AvatarStatus,
} from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"

const sizes = ["xs", "sm", "default", "lg", "xl"] as const
const shapes = ["circle", "square"] as const
const statuses: AvatarStatus[] = ["online", "away", "busy", "offline"]

export function AvatarStatusDemo() {
  const [index, setIndex] = React.useState(0)
  const status = statuses[index]

  return (
    <div className="flex flex-col gap-4">
      {shapes.map((shape) => (
        <div key={shape} className="flex flex-wrap items-center gap-3">
          {sizes.map((size) => (
            <Avatar key={size} size={size} shape={shape}>
              <AvatarImage src="/preview/landscape.svg" alt="" />
              <AvatarFallback>GH</AvatarFallback>
              <AvatarBadge status={status} />
            </Avatar>
          ))}
          <Avatar size="lg" shape={shape}>
            <AvatarFallback>AT</AvatarFallback>
            <AvatarBadge>
              <IconCheck />
            </AvatarBadge>
          </Avatar>
        </div>
      ))}
      <div className="flex items-center gap-3">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setIndex((index + 1) % statuses.length)}
        >
          Next status
        </Button>
        <span className="text-sm text-muted-foreground">{status}</span>
      </div>
    </div>
  )
}

Group

<AvatarGroup /> overlaps its avatars and sets their size and shape. max collapses the rest into a count.

ALATGH5 more
ALATGH5 more
ALATGH5 more
ALATGH5 more
ALATGH5 more
ALATGHKJ4 more
"use client"

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
  getInitials,
} from "@/components/ui/avatar"

const people = [
  { name: "Ada Lovelace", image: "/preview/landscape.svg" },
  { name: "Alan Turing", image: "/preview/landscape.svg" },
  { name: "Grace Hopper", image: "/preview/landscape.svg" },
  { name: "Katherine Johnson", image: null },
  { name: "Linus Torvalds", image: null },
  { name: "Margaret Hamilton", image: null },
  { name: "Tim Berners-Lee", image: null },
  { name: "Barbara Liskov", image: null },
]

const sizes = ["xs", "sm", "default", "lg", "xl"] as const

export function AvatarGroupDemo() {
  return (
    <div className="flex flex-col items-start gap-4">
      {sizes.map((size) => (
        <AvatarGroup key={size} size={size} max={4}>
          {people.map((person) => (
            <Avatar key={person.name}>
              {person.image ? <AvatarImage src={person.image} alt="" /> : null}
              <AvatarFallback>{getInitials(person.name)}</AvatarFallback>
            </Avatar>
          ))}
        </AvatarGroup>
      ))}
      <AvatarGroup shape="square" max={5}>
        {people.map((person) => (
          <Avatar key={person.name}>
            {person.image ? <AvatarImage src={person.image} alt="" /> : null}
            <AvatarFallback>{getInitials(person.name)}</AvatarFallback>
          </Avatar>
        ))}
      </AvatarGroup>
    </div>
  )
}

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.

ALATGHLTMH3 more
"use client"

import {
  Avatar,
  AvatarFallback,
  AvatarGroup,
  AvatarGroupCount,
  getInitials,
} from "@/components/ui/avatar"

const team = [
  "Ada Lovelace",
  "Alan Turing",
  "Grace Hopper",
  "Linus Torvalds",
  "Margaret Hamilton",
]

export function AvatarLinkedGroup() {
  return (
    <AvatarGroup aria-label="Team" size="lg">
      {team.map((name) => (
        <Avatar key={name} render={<a href="#" aria-label={name} />}>
          <AvatarFallback>{getInitials(name)}</AvatarFallback>
        </Avatar>
      ))}
      <AvatarGroupCount count={3} />
    </AvatarGroup>
  )
}

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.

ALSupercalifragilisticexpialidocious-team-workspace-name
KJKJWWWWWWWW
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarImage,
} from "@/components/ui/avatar"

export function AvatarLayout() {
  return (
    <div className="flex flex-col items-start gap-4">
      <div className="flex w-64 max-w-full items-center gap-2 rounded-lg border p-2 text-sm">
        <Avatar>
          <AvatarImage src="/preview/landscape.svg" alt="" />
          <AvatarFallback>AL</AvatarFallback>
        </Avatar>
        <span className="min-w-0 truncate">
          Supercalifragilisticexpialidocious-team-workspace-name
        </span>
      </div>
      <div className="flex flex-wrap items-center gap-3">
        <Avatar className="size-20">
          <AvatarImage src="/preview/landscape.svg" alt="" />
          <AvatarFallback>KJ</AvatarFallback>
        </Avatar>
        <Avatar className="size-20" shape="square">
          <AvatarFallback>KJ</AvatarFallback>
          <AvatarBadge status="online" />
        </Avatar>
        <Avatar size="sm">
          <AvatarFallback>WWWWWWWW</AvatarFallback>
        </Avatar>
      </div>
    </div>
  )
}

Right to left

The badge stays on the end corner, which is the left in RTL, and groups overlap from the right.

مع
ALATGH5 more
"use client"

import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarGroup,
  AvatarImage,
  getInitials,
} from "@/components/ui/avatar"

const people = [
  { name: "Ada Lovelace", image: "/preview/landscape.svg" },
  { name: "Alan Turing", image: "/preview/landscape.svg" },
  { name: "Grace Hopper", image: "/preview/landscape.svg" },
  { name: "Katherine Johnson", image: null },
  { name: "Linus Torvalds", image: null },
  { name: "Margaret Hamilton", image: null },
  { name: "Tim Berners-Lee", image: null },
  { name: "Barbara Liskov", image: null },
]

export function AvatarRtl() {
  return (
    <div dir="rtl" className="flex flex-wrap items-center gap-4">
      <Avatar size="lg">
        <AvatarFallback>{getInitials("محمد علي")}</AvatarFallback>
        <AvatarBadge status="online" />
      </Avatar>
      <AvatarGroup max={4}>
        {people.map((person) => (
          <Avatar key={person.name}>
            {person.image ? <AvatarImage src={person.image} alt="" /> : null}
            <AvatarFallback>{getInitials(person.name)}</AvatarFallback>
          </Avatar>
        ))}
      </AvatarGroup>
    </div>
  )
}

Avatars aren’t focusable on their own. Rendered as a link or button they get the usual keys.

KeyAction
TabMoves focus to the next linked avatar.
EnterFollows 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 status are 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.

PropTypeDefault
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>
AttributeDescription
data-slot="avatar"Target avatars in CSS.
data-sizeThe resolved size.
data-shapeThe resolved shape.
--avatar-radiusThe corner radius, shared by every layer.
PropTypeDefault
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.
booleanfalse
render
ReactElement | (props, state) => ReactElement<img>
AttributeDescription
data-slot="avatar-image"Target images in CSS.
data-loadingPresent while the image loads.
data-errorPresent when the image failed to load.
data-starting-stylePresent while the image fades in.
data-ending-stylePresent while the image fades out.
PropTypeDefault
childrenEmpty or whitespace shows the user icon.
ReactNode<IconUser />
delayMilliseconds to wait before showing it.
number0
render
ReactElement | (props, state) => ReactElement<span>
AttributeDescription
data-slot="avatar-fallback"Target fallbacks in CSS.
data-readyfalse until the delay has passed.
PropTypeDefault
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–
AttributeDescription
data-slot="avatar-badge"Target badges in CSS.
data-statusThe current status.
data-slot="avatar-badge-pulse"The pulse played after a status change.
PropTypeDefault
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–
AttributeDescription
data-slot="avatar-group"Target groups in CSS.
data-sizeThe group’s size.
PropTypeDefault
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"–
AttributeDescription
data-slot="avatar-group-count"Target the count in CSS.
data-sizeThe resolved size.
data-shapeThe 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.