# Avatar

An image element with a fallback for representing the user.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  Avatar,
  AvatarBadge,
  AvatarFallback,
  AvatarGroup,
  AvatarGroupCount,
  AvatarImage,
} from "@/components/ui/avatar"
import { HStack } from "@/components/ui/stack"
import { color } from "@/theme/tokens.stylex"

const styles = stylex.create({
  online: { backgroundColor: color.calloutSuccessAccent },
})

export default function AvatarDemo() {
  return (
    <HStack align="center" gap={6} wrap="wrap">
      <Avatar>
        <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
        <AvatarFallback>CN</AvatarFallback>
      </Avatar>
      <Avatar>
        <AvatarImage src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
        <AvatarFallback>ER</AvatarFallback>
        <AvatarBadge xstyle={styles.online} />
      </Avatar>
      <AvatarGroup>
        <Avatar>
          <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
          <AvatarFallback>CN</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarImage src="https://github.com/maxleiter.png" alt="@maxleiter" />
          <AvatarFallback>LR</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarImage src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
          <AvatarFallback>ER</AvatarFallback>
        </Avatar>
        <AvatarGroupCount>+3</AvatarGroupCount>
      </AvatarGroup>
    </HStack>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation). The avatar also needs `interop.css`, which
   sizes the icon inside a badge or a count.

3. Copy and paste the following code into your project.

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

   import { useRender } from "@base-ui/react/use-render"
   import { createContext, use } from "react"
   import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { color, layer, leading, radius, space, text } from "@/theme/tokens.stylex"

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

   const AvatarContext = createContext<{ size: AvatarSize } | null>(null)

   const AvatarGroupContext = createContext<{ size: AvatarSize } | null>(null)

   const styles = stylex.create({
     root: {
       position: "relative",
       display: "flex",
       flexShrink: 0,
       userSelect: "none",
       borderRadius: radius.full,
       "::after": {
         content: "''",
         position: "absolute",
         inset: 0,
         borderRadius: radius.full,
         borderWidth: 1,
         borderStyle: "solid",
         borderColor: `color-mix(in oklab, ${color.foreground} 10%, transparent)`,
       },
     },
     image: {
       aspectRatio: "1 / 1",
       width: "100%",
       height: "100%",
       objectFit: "cover",
       borderRadius: radius.full,
     },
     fallback: {
       display: "flex",
       width: "100%",
       height: "100%",
       alignItems: "center",
       justifyContent: "center",
       borderRadius: radius.full,
       backgroundColor: color.muted,
       color: color.mutedForeground,
     },
     badge: {
       position: "absolute",
       insetInlineEnd: 0,
       insetBlockEnd: 0,
       zIndex: layer.raised,
       display: "inline-flex",
       alignItems: "center",
       justifyContent: "center",
       borderRadius: radius.full,
       backgroundColor: color.primary,
       color: color.primaryForeground,
       boxShadow: `0 0 0 2px var(--carbon-avatar-cutout, ${color.background})`,
     },
     group: { display: "flex", alignItems: "center" },
     count: {
       position: "relative",
       display: "flex",
       flexShrink: 0,
       alignItems: "center",
       justifyContent: "center",
       userSelect: "none",
       borderRadius: radius.full,
       backgroundColor: color.muted,
       "--carbon-avatar-surface": `inset 0 0 0 1px color-mix(in oklab, ${color.foreground} 10%, transparent)`,
       boxShadow: "var(--carbon-avatar-surface)",
       color: color.mutedForeground,
     },
     member: {
       marginInlineStart: {
         default: `calc(-1 * ${space.s2})`,
         ":nth-child(1 of [data-slot])": 0,
       },
       boxShadow: `var(--carbon-avatar-surface, 0 0 ${color.transparent}), 0 0 0 2px var(--carbon-avatar-cutout, ${color.background})`,
     },
   })

   const sizes = stylex.create({
     sm: { width: space.s6, height: space.s6 },
     default: { width: space.s8, height: space.s8 },
     lg: { width: space.s10, height: space.s10 },
   })

   const fallbackSizes = stylex.create({
     sm: { fontSize: text.xs, lineHeight: leading.xs },
     default: { fontSize: text.sm, lineHeight: leading.sm },
     lg: { fontSize: text.sm, lineHeight: leading.sm },
   })

   const badgeSizes = stylex.create({
     sm: { width: space.s2, height: space.s2, "--carbon-icon-size": space.s0 },
     default: { width: space.s2_5, height: space.s2_5, "--carbon-icon-size": space.s2 },
     lg: { width: space.s3, height: space.s3, "--carbon-icon-size": space.s2 },
   })

   const countSizes = stylex.create({
     sm: {
       width: space.s6,
       height: space.s6,
       fontSize: text.xs,
       lineHeight: leading.xs,
       "--carbon-icon-size": space.s3,
     },
     default: {
       width: space.s8,
       height: space.s8,
       fontSize: text.sm,
       lineHeight: leading.sm,
       "--carbon-icon-size": space.s4,
     },
     lg: {
       width: space.s10,
       height: space.s10,
       fontSize: text.sm,
       lineHeight: leading.sm,
       "--carbon-icon-size": space.s5,
     },
   })

   const avatarStyles = {
     base: styles.root,
     sizes,
     image: styles.image,
     fallback: styles.fallback,
     fallbackSizes,
     badge: styles.badge,
     badgeSizes,
     group: styles.group,
     count: styles.count,
     countSizes,
     member: styles.member,
   }

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type AvatarProps = Omit<AvatarPrimitive.Root.Props, "className"> & Overrides & { size?: AvatarSize }

   function Avatar({ size, xstyle, className, ...props }: AvatarProps) {
     const group = use(AvatarGroupContext)
     const resolved = size ?? group?.size ?? "default"
     const sx = stylex.props(styles.root, sizes[resolved], group && styles.member, xstyle)

     return (
       <AvatarContext value={{ size: resolved }}>
         <AvatarPrimitive.Root
           data-slot="avatar"
           data-size={resolved}
           {...sx}
           {...props}
           className={clsx(sx.className, className)}
         />
       </AvatarContext>
     )
   }

   type AvatarImageProps = Omit<AvatarPrimitive.Image.Props, "className"> & Overrides

   function AvatarImage({ xstyle, className, ...props }: AvatarImageProps) {
     const sx = stylex.props(styles.image, xstyle)
     return (
       <AvatarPrimitive.Image
         data-slot="avatar-image"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type AvatarFallbackProps = Omit<AvatarPrimitive.Fallback.Props, "className"> & Overrides

   function AvatarFallback({ xstyle, className, ...props }: AvatarFallbackProps) {
     const size = use(AvatarContext)?.size ?? "default"
     const sx = stylex.props(styles.fallback, fallbackSizes[size] ?? fallbackSizes.default, xstyle)

     return (
       <AvatarPrimitive.Fallback
         data-slot="avatar-fallback"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   function AvatarBadge({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"span"> & Overrides) {
     const size = use(AvatarContext)?.size ?? "default"
     const sx = stylex.props(styles.badge, badgeSizes[size] ?? badgeSizes.default, xstyle)

     return useRender({
       defaultTagName: "span",
       render,
       ref,
       props: {
         "data-slot": "avatar-badge",
         "data-size": size,
         ...sx,
         ...props,
         className: clsx("carbon-icons", sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   type AvatarGroupProps = useRender.ComponentProps<"div"> & Overrides & { size?: AvatarSize }

   function AvatarGroup({
     render,
     ref,
     style,
     size = "default",
     xstyle,
     className,
     ...props
   }: AvatarGroupProps) {
     const sx = stylex.props(styles.group, xstyle)

     return (
       <AvatarGroupContext value={{ size }}>
         {useRender({
           defaultTagName: "div",
           render,
           ref,
           props: {
             "data-slot": "avatar-group",
             "data-size": size,
             ...sx,
             ...props,
             className: clsx(sx.className, className),
             style: { ...sx.style, ...style },
           },
         })}
       </AvatarGroupContext>
     )
   }

   function AvatarGroupCount({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & Overrides) {
     const group = use(AvatarGroupContext)
     const size = group?.size ?? "default"
     const sx = stylex.props(
       styles.count,
       countSizes[size] ?? countSizes.default,
       group && styles.member,
       xstyle,
     )

     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "avatar-group-count",
         "data-size": size,
         ...sx,
         ...props,
         className: clsx("carbon-icons", sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   export {
     Avatar,
     AvatarBadge,
     AvatarFallback,
     AvatarGroup,
     AvatarGroupCount,
     AvatarImage,
     avatarStyles,
     type AvatarSize,
   }
   ```

4. Update the import paths to match your project setup.

## Usage

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

```tsx
<Avatar>
  <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
  <AvatarFallback>CN</AvatarFallback>
</Avatar>
```

## Composition

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

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

## Basic

An avatar with an image and a fallback. The fallback shows while the image
loads and stays if it fails.

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

export default function AvatarBasic() {
  return (
    <Avatar>
      <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
      <AvatarFallback>CN</AvatarFallback>
    </Avatar>
  )
}
```

## Badge

Use `AvatarBadge` for a dot in the avatar's bottom corner. Pass `xstyle` to
color it.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { color } from "@/theme/tokens.stylex"

const styles = stylex.create({
  online: { backgroundColor: color.calloutSuccessAccent },
})

export default function AvatarWithBadge() {
  return (
    <Avatar>
      <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
      <AvatarFallback>CN</AvatarFallback>
      <AvatarBadge xstyle={styles.online} />
    </Avatar>
  )
}
```

## Badge with icon

Put an icon inside `AvatarBadge` at `default` or `lg`.

```tsx
import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Plus } from "lucide-react"

export default function AvatarBadgeIcon() {
  return (
    <Avatar size="lg">
      <AvatarImage src="https://github.com/pranathip.png" alt="@pranathip" />
      <AvatarFallback>PP</AvatarFallback>
      <AvatarBadge>
        <Plus />
      </AvatarBadge>
    </Avatar>
  )
}
```

> **Note**
>
> At `sm` the badge is 8px across and sizes any icon to nothing. Use a plain dot
> at that size.

## Avatar group

Use `AvatarGroup` to overlap a set of avatars. Each member sits under the one
before it, with a ring that separates the two.

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

export default function AvatarGroupDemo() {
  return (
    <AvatarGroup>
      <Avatar>
        <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
        <AvatarFallback>CN</AvatarFallback>
      </Avatar>
      <Avatar>
        <AvatarImage src="https://github.com/maxleiter.png" alt="@maxleiter" />
        <AvatarFallback>LR</AvatarFallback>
      </Avatar>
      <Avatar>
        <AvatarImage src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
        <AvatarFallback>ER</AvatarFallback>
      </Avatar>
    </AvatarGroup>
  )
}
```

The ring, and the one around `AvatarBadge`, is drawn in the page colour. On
another surface, such as a card or a popover, set `--carbon-avatar-cutout` to
that surface's fill on the group or any ancestor.

```tsx
const styles = stylex.create({
  onCard: { "--carbon-avatar-cutout": color.card },
})

<AvatarGroup xstyle={styles.onCard}>…</AvatarGroup>
```

## Avatar group count

Use `AvatarGroupCount` for the members the group does not show.

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

export default function AvatarGroupCountDemo() {
  return (
    <AvatarGroup>
      <Avatar>
        <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
        <AvatarFallback>CN</AvatarFallback>
      </Avatar>
      <Avatar>
        <AvatarImage src="https://github.com/maxleiter.png" alt="@maxleiter" />
        <AvatarFallback>LR</AvatarFallback>
      </Avatar>
      <Avatar>
        <AvatarImage src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
        <AvatarFallback>ER</AvatarFallback>
      </Avatar>
      <AvatarGroupCount>+3</AvatarGroupCount>
    </AvatarGroup>
  )
}
```

## Avatar group with icon

An icon works in the count too.

```tsx
import { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage } from "@/components/ui/avatar"
import { Plus } from "lucide-react"

export default function AvatarGroupCountIcon() {
  return (
    <AvatarGroup>
      <Avatar>
        <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
        <AvatarFallback>CN</AvatarFallback>
      </Avatar>
      <Avatar>
        <AvatarImage src="https://github.com/maxleiter.png" alt="@maxleiter" />
        <AvatarFallback>LR</AvatarFallback>
      </Avatar>
      <Avatar>
        <AvatarImage src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
        <AvatarFallback>ER</AvatarFallback>
      </Avatar>
      <AvatarGroupCount>
        <Plus />
      </AvatarGroupCount>
    </AvatarGroup>
  )
}
```

## Sizes

Use the `size` prop to change the size of the avatar. `AvatarGroup` takes the
same prop and sizes every member that does not name its own.

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

export default function AvatarSize() {
  return (
    <HStack align="center" gap={2} wrap="wrap">
      <Avatar size="sm">
        <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
        <AvatarFallback>CN</AvatarFallback>
      </Avatar>
      <Avatar size="default">
        <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
        <AvatarFallback>CN</AvatarFallback>
      </Avatar>
      <Avatar size="lg">
        <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
        <AvatarFallback>CN</AvatarFallback>
      </Avatar>
    </HStack>
  )
}
```

## Dropdown

Render a [`DropdownMenu`](https://carbon.style/docs/components/dropdown-menu) trigger as a `Button`
and put the avatar inside it.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { radius } from "@/theme/tokens.stylex"

const styles = stylex.create({
  trigger: { borderRadius: radius.full },
  menu: { minWidth: "8rem" },
})

export default function AvatarDropdown() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger
        render={
          <Button
            variant="ghost"
            size="icon"
            xstyle={styles.trigger}
            aria-label="Open account menu"
          />
        }
      >
        <Avatar>
          <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
          <AvatarFallback>CN</AvatarFallback>
        </Avatar>
      </DropdownMenuTrigger>
      <DropdownMenuContent xstyle={styles.menu}>
        <DropdownMenuGroup>
          <DropdownMenuItem>Profile</DropdownMenuItem>
          <DropdownMenuItem>Billing</DropdownMenuItem>
          <DropdownMenuItem>Settings</DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive">Log out</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

Give an avatar-only trigger an `aria-label`. There is no text for a screen
reader to announce.

## API reference

Pass `render` to replace a part's element or compose it with another component.

`Avatar`, `AvatarImage` and `AvatarFallback` wrap the Base UI `Avatar` part of
the same name and accept its props. See the
[Base UI reference](https://base-ui.com/react/components/avatar). Every part
also takes `xstyle` and `className`.

### Avatar

Renders a `span`.

| Prop        | Type                        | Default                       |
| ----------- | --------------------------- | ----------------------------- |
| `size`      | `"sm" \| "default" \| "lg"` | the group's, else `"default"` |
| `xstyle`    | `StyleXStyles`              | —                             |
| `className` | `string`                    | —                             |

The sizes are 24px, 32px and 40px. The avatar passes the resolved size to the
fallback and the badge through context.

### AvatarImage

Renders an `img`. Takes `src`, `alt` and `onLoadingStatusChange`.

### AvatarFallback

Renders a `span`. `delay` is the number of milliseconds to wait before showing
it, so initials never flash before a fast image.

### AvatarBadge

Renders a `span`, positioned in the avatar's bottom corner and sized from the
avatar's `size`.

### AvatarGroup

Renders a `div`.

| Prop   | Type                        | Default     |
| ------ | --------------------------- | ----------- |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` |

### AvatarGroupCount

Renders a `div`, sized from the group's `size`.

### avatarStyles

The style maps behind the component. Spread them onto any element.

| Key             | What it is                                     |
| --------------- | ---------------------------------------------- |
| `base`          | The circle, the hairline over the image        |
| `sizes`         | One entry per `size`                           |
| `image`         | The cover fit and the circle                   |
| `fallback`      | The flat fill behind initials                  |
| `fallbackSizes` | The fallback's type, keyed by `size`           |
| `badge`         | The dot in the corner                          |
| `badgeSizes`    | The dot's size and icon size, keyed by `size`  |
| `group`         | The group's row                                |
| `count`         | The tally's fill, hairline and shape           |
| `countSizes`    | The tally's size and type, keyed by `size`     |
| `member`        | The overlap and ring a member in a group draws |

### Data attributes

| Attribute                        | On           | Effect                             |
| -------------------------------- | ------------ | ---------------------------------- |
| `data-slot="avatar"`             | the root     | Marks a member of a group          |
| `data-slot="avatar-group-count"` | the tally    | Marks a member of a group          |
| `data-size`                      | every part   | Reflects the resolved `size`       |
| `data-slot="avatar-image"`       | the image    | Targets the image from a parent    |
| `data-slot="avatar-fallback"`    | the fallback | Targets the fallback from a parent |
| `data-slot="avatar-badge"`       | the badge    | Targets the badge from a parent    |
| `data-slot="avatar-group"`       | the group    | Targets the group from a parent    |

The first member in a group keeps its leading margin, and that exemption is
keyed on `data-slot` rather than `:first-child`, so anything else you put first
in the group does not claim it.

### CSS variables

| Variable             | On              | Effect                                 |
| -------------------- | --------------- | -------------------------------------- |
| `--carbon-icon-size` | badge and tally | The icon's size, read by `interop.css` |
