# Avatar

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

Docs: https://hextaui.com/docs/avatar
Markdown: https://hextaui.com/docs/avatar.md

```tsx title="components/examples/avatar/demo.tsx"
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>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/avatar.json
```

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

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/avatar.tsx"
"use client"

import * as React from "react"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { IconUser } from "@tabler/icons-react"
import { cva } from "class-variance-authority"
import { cn } from "cn"

type AvatarSize = "xs" | "sm" | "default" | "lg" | "xl"

type AvatarShape = "circle" | "square"

type AvatarStatus = "online" | "away" | "busy" | "offline"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

const AvatarGroupContext = React.createContext<{
  size?: AvatarSize
  shape?: AvatarShape
} | null>(null)

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const avatarVariants = cva(
  "group/avatar [container-type:size] relative isolate inline-flex shrink-0 rounded-(--avatar-radius) bg-muted align-middle text-muted-foreground outline-none select-none after:pointer-events-none after:absolute after:inset-0 after:z-2 after:rounded-[inherit] after:inset-ring-(length:--hairline) after:inset-ring-foreground/10 focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden",
  {
    variants: {
      size: {
        xs: "size-5",
        sm: "size-6",
        default: "size-8",
        lg: "size-10",
        xl: "size-14",
      },
      shape: {
        circle: "[--avatar-radius:9999px]",
        square: "",
      },
    },
    compoundVariants: [
      {
        shape: "square",
        size: "xs",
        className: "[--avatar-radius:var(--radius-sm)]",
      },
      {
        shape: "square",
        size: "sm",
        className: "[--avatar-radius:var(--radius-md)]",
      },
      {
        shape: "square",
        size: ["default", "lg"],
        className: "[--avatar-radius:var(--radius-lg)]",
      },
      {
        shape: "square",
        size: "xl",
        className: "[--avatar-radius:var(--radius-xl)]",
      },
    ],
    defaultVariants: {
      size: "default",
      shape: "circle",
    },
  }
)

type AvatarProps = AvatarPrimitive.Root.Props & {
  size?: AvatarSize
  shape?: AvatarShape
}

function Avatar({ className, size, shape, ...props }: AvatarProps) {
  const group = React.useContext(AvatarGroupContext)
  const resolvedSize = size ?? group?.size ?? "default"
  const resolvedShape = shape ?? group?.shape ?? "circle"

  return (
    <AvatarPrimitive.Root
      data-slot="avatar"
      data-size={resolvedSize}
      data-shape={resolvedShape}
      className={mergeClassName(
        cn(
          avatarVariants({ size: resolvedSize, shape: resolvedShape }),
          group && "ring-2 ring-background"
        ),
        className
      )}
      {...props}
    />
  )
}

function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
  return (
    <AvatarPrimitive.Image
      data-slot="avatar-image"
      className={mergeClassName(
        "absolute inset-0 z-1 size-full rounded-(--avatar-radius) object-cover transition-opacity duration-200 ease-out-quint data-ending-style:opacity-0 data-error:invisible data-loading:invisible data-starting-style:opacity-0 motion-reduce:transition-none",
        className
      )}
      {...props}
    />
  )
}

function isEmpty(children: React.ReactNode) {
  return (
    children === undefined ||
    children === null ||
    typeof children === "boolean" ||
    (typeof children === "string" && children.trim() === "")
  )
}

type AvatarFallbackProps = useRender.ComponentProps<"span"> & {
  delay?: number
}

function AvatarFallback({
  className,
  children,
  delay = 0,
  render,
  ...props
}: AvatarFallbackProps) {
  const [delayPassed, setDelayPassed] = React.useState(delay <= 0)

  React.useEffect(() => {
    if (delay <= 0) {
      return
    }

    const timeout = window.setTimeout(() => setDelayPassed(true), delay)

    return () => window.clearTimeout(timeout)
  }, [delay])

  const ready = delayPassed || delay <= 0

  return useRender({
    defaultTagName: "span",
    render,
    props: mergeProps<"span">(
      {
        className: cn(
          "absolute inset-0 flex items-center justify-center overflow-hidden rounded-(--avatar-radius) bg-muted text-[max(0.5625rem,38cqmin)] leading-none font-medium whitespace-nowrap text-muted-foreground transition-[opacity,visibility] duration-200 ease-out-quint data-[ready=false]:invisible data-[ready=false]:opacity-0 motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-[56cqmin] [[data-slot=avatar]:has(>[data-slot=avatar-image]:not([data-loading]):not([data-error]):not([data-ending-style]))>&]:invisible [[data-slot=avatar]:has(>[data-slot=avatar-image]:not([data-loading]):not([data-error]):not([data-ending-style]))>&]:opacity-0",
          className
        ),
        children: isEmpty(children) ? <IconUser aria-hidden /> : children,
      },
      props,
      { "data-slot": "avatar-fallback", "data-ready": String(ready) } as Record<
        string,
        string
      >
    ),
  })
}

const statusLabels: Record<AvatarStatus, string> = {
  online: "Online",
  away: "Away",
  busy: "Busy",
  offline: "Offline",
}

const avatarBadgeVariants = cva(
  "absolute end-(--avatar-badge-inset) bottom-(--avatar-badge-inset) z-3 inline-flex size-(--avatar-badge-size) items-center justify-center rounded-full ring-2 ring-background select-none [--avatar-badge-inset:calc(min(var(--avatar-radius),50cqmin)*0.2929-var(--avatar-badge-size)/2)] [--avatar-badge-size:max(0.375rem,min(0.875rem,30cqmin))] [&>svg]:pointer-events-none [&>svg]:size-[calc(var(--avatar-badge-size)*0.75)] group-data-[size=sm]/avatar:[&>svg]:hidden group-data-[size=xs]/avatar:[&>svg]:hidden",
  {
    variants: {
      status: {
        none: "bg-primary text-primary-foreground",
        online: "bg-success",
        away: "bg-warning",
        busy: "bg-destructive",
        offline:
          "border-[max(1.5px,calc(var(--avatar-badge-size)*0.22))] border-muted-foreground bg-background",
      },
    },
    defaultVariants: {
      status: "none",
    },
  }
)

type AvatarBadgeProps = React.ComponentProps<"span"> & {
  status?: AvatarStatus
}

function AvatarBadge({
  className,
  status,
  children,
  ...props
}: AvatarBadgeProps) {
  const [pulse, setPulse] = React.useState({ status, count: 0 })

  if (status !== pulse.status) {
    setPulse({ status, count: pulse.count + 1 })
  }

  const showPulse = pulse.count > 0 && status && status !== "offline"

  return (
    <span
      data-slot="avatar-badge"
      data-status={status}
      role={status ? "img" : undefined}
      aria-label={status ? statusLabels[status] : undefined}
      className={cn(
        avatarBadgeVariants({ status: status ?? "none" }),
        className
      )}
      {...props}
    >
      {showPulse ? (
        <span
          key={pulse.count}
          data-slot="avatar-badge-pulse"
          aria-hidden
          className="pointer-events-none absolute inset-0 rounded-[inherit] bg-inherit motion-safe:animate-avatar-badge-pulse motion-reduce:hidden"
        />
      ) : null}
      {children}
    </span>
  )
}

const avatarGroupVariants = cva(
  "group/avatar-group isolate flex w-fit max-w-full items-center",
  {
    variants: {
      size: {
        xs: "-space-x-1.5",
        sm: "-space-x-2",
        default: "-space-x-2",
        lg: "-space-x-2.5",
        xl: "-space-x-3.5",
      },
    },
    defaultVariants: {
      size: "default",
    },
  }
)

type AvatarGroupProps = React.ComponentProps<"div"> & {
  size?: AvatarSize
  shape?: AvatarShape
  max?: number
}

function resolveMax(max: number | undefined) {
  return max !== undefined && Number.isFinite(max) && max >= 1
    ? Math.max(2, Math.floor(max))
    : Infinity
}

function AvatarGroup({
  className,
  size = "default",
  shape = "circle",
  max,
  children,
  ...props
}: AvatarGroupProps) {
  const context = React.useMemo(() => ({ size, shape }), [size, shape])
  const items = React.Children.toArray(children)
  const limit = resolveMax(max)
  const overflow = items.length > limit ? items.length - limit + 1 : 0

  return (
    <AvatarGroupContext.Provider value={context}>
      <div
        data-slot="avatar-group"
        data-size={size}
        role="group"
        className={cn(avatarGroupVariants({ size }), className)}
        {...props}
      >
        {overflow ? items.slice(0, limit - 1) : items}
        {overflow ? <AvatarGroupCount count={overflow} /> : null}
      </div>
    </AvatarGroupContext.Provider>
  )
}

function formatCount(count: number) {
  return count > 99 ? "99+" : `+${count}`
}

type AvatarGroupCountProps = React.ComponentProps<"span"> & {
  count?: number
  size?: AvatarSize
  shape?: AvatarShape
}

function AvatarGroupCount({
  className,
  count,
  size,
  shape,
  children,
  ...props
}: AvatarGroupCountProps) {
  const group = React.useContext(AvatarGroupContext)
  const resolvedSize = size ?? group?.size ?? "default"
  const resolvedShape = shape ?? group?.shape ?? "circle"
  const value =
    count !== undefined && Number.isFinite(count)
      ? Math.max(0, Math.floor(count))
      : undefined

  return (
    <span
      data-slot="avatar-group-count"
      data-size={resolvedSize}
      data-shape={resolvedShape}
      className={cn(
        avatarVariants({ size: resolvedSize, shape: resolvedShape }),
        group && "ring-2 ring-background",
        className
      )}
      {...props}
    >
      <span className="flex size-full items-center justify-center overflow-hidden rounded-[inherit] text-[max(0.5625rem,36cqmin)] leading-none font-medium whitespace-nowrap tabular-nums [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-[50cqmin]">
        {value !== undefined && children === undefined ? (
          <>
            <span aria-hidden dir="ltr">
              {formatCount(value)}
            </span>
            <span className="sr-only">{`${value} more`}</span>
          </>
        ) : (
          children
        )}
      </span>
    </span>
  )
}

const segmenter =
  typeof Intl !== "undefined" && "Segmenter" in Intl
    ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
    : null

const visible = /[\p{L}\p{N}\p{Extended_Pictographic}]/u

const markless = /^[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}]/u

function graphemes(word: string) {
  return segmenter
    ? Array.from(segmenter.segment(word), (part) => part.segment)
    : Array.from(word)
}

function firstGrapheme(word: string) {
  const grapheme = graphemes(word.slice(0, 64)).find((part) =>
    visible.test(part)
  )

  if (!grapheme) {
    return ""
  }

  return markless.test(grapheme) ? grapheme.replace(/\p{M}+/gu, "") : grapheme
}

function getInitials(name: string | null | undefined, max = 2) {
  if (typeof name !== "string") {
    return ""
  }

  const limit = Number.isFinite(max) ? Math.max(1, Math.floor(max)) : 2
  const source = name.trim()
  const at = source.indexOf("@")
  const words =
    at > 0
      ? source
          .slice(0, at)
          .split("+")[0]
          .split(/[._-]+/u)
      : source.split(/\s+/u)
  const letters: string[] = []

  for (const word of words) {
    const letter = firstGrapheme(word)

    if (letter) {
      letters.push(letter)
    }
  }

  const picked =
    letters.length <= limit
      ? letters
      : limit === 1
        ? letters.slice(0, 1)
        : [...letters.slice(0, limit - 1), letters[letters.length - 1]]

  return picked.join("").toLocaleUpperCase()
}

export {
  Avatar,
  AvatarImage,
  AvatarFallback,
  AvatarBadge,
  AvatarGroup,
  AvatarGroupCount,
  avatarVariants,
  avatarBadgeVariants,
  getInitials,
  type AvatarProps,
  type AvatarFallbackProps,
  type AvatarBadgeProps,
  type AvatarGroupProps,
  type AvatarGroupCountProps,
  type AvatarSize,
  type AvatarShape,
  type AvatarStatus,
}
```

Update the import paths to match your project setup.

## Usage

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

```tsx
<Avatar>
  <AvatarImage src="/ada.jpg" alt="" />
  <AvatarFallback>AL</AvatarFallback>
</Avatar>
```

## Composition

```text
Avatar
├── AvatarImage
├── AvatarFallback
└── AvatarBadge

AvatarGroup
├── Avatar
└── AvatarGroupCount
```

## Examples

### 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.

```tsx title="components/examples/avatar/sizes.tsx"
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.

```tsx title="components/examples/avatar/loading.tsx"
"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.

```tsx title="components/examples/avatar/initials.tsx"
"use client"

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

const names = [
  "Ada Lovelace",
  "Madonna",
  "jean-luc picard",
  "ada.lovelace+news@example.com",
  "(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.

```tsx title="components/examples/avatar/status.tsx"
"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.

```tsx title="components/examples/avatar/group.tsx"
"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.

```tsx title="components/examples/avatar/linked-group.tsx"
"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.

```tsx title="components/examples/avatar/layout.tsx"
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.

```tsx title="components/examples/avatar/rtl.tsx"
"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>
  )
}
```

## Keyboard

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. |

## Accessibility

- 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.

## API reference

Built on the Base UI avatar. Every part accepts the attributes of the element it renders. The styles are exported as `avatarVariants` and `avatarBadgeVariants`.

### Avatar

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"xs" \| "sm" \| "default" \| "lg" \| "xl"` | `"default"` | Inherited from the group when omitted. |
| `shape` | `"circle" \| "square"` | `"circle"` | Inherited from the group when omitted. |
| `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. |

### AvatarImage

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `src` | `string` | – |  |
| `alt` | `string` | – |  |
| `onLoadingStatusChange` | `(status: "idle" \| "loading" \| "loaded" \| "error") => void` | – |  |
| `keepMounted` | `boolean` | `false` | Load the image in place instead of preloading it, for loading="lazy" or next/image. |
| `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. |

### AvatarFallback

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | `<IconUser />` | Empty or whitespace shows the user icon. |
| `delay` | `number` | `0` | Milliseconds to wait before showing it. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<span>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="avatar-fallback"` | Target fallbacks in CSS. |
| `data-ready` | false until the delay has passed. |

### AvatarBadge

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `status` | `"online" \| "away" \| "busy" \| "offline"` | – | Colors the dot and labels it for assistive tech. Without it the badge uses the primary color. |
| `children` | `ReactNode` | – | An icon inside the badge. Hidden at the xs and sm sizes. |

| 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. |

### AvatarGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"xs" \| "sm" \| "default" \| "lg" \| "xl"` | `"default"` |  |
| `shape` | `"circle" \| "square"` | `"circle"` |  |
| `max` | `number` | – | How many items to show, including the count. Values below 2 are raised to 2. |

| Attribute | Description |
| --- | --- |
| `data-slot="avatar-group"` | Target groups in CSS. |
| `data-size` | The group’s size. |

### AvatarGroupCount

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `count` | `number` | – | Shown as +3, or 99+ above 99. |
| `children` | `ReactNode` | – | Replaces the count, for example with an icon. |
| `size` | `"xs" \| "sm" \| "default" \| "lg" \| "xl"` | – | Inherited from the group when omitted. |
| `shape` | `"circle" \| "square"` | – | Inherited from the group when omitted. |

| Attribute | Description |
| --- | --- |
| `data-slot="avatar-group-count"` | Target the count in CSS. |
| `data-size` | The resolved size. |
| `data-shape` | The resolved shape. |

### getInitials

`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.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
