Avatar

An image element with a fallback for representing the user.

CNER
CNLRER
+3
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

Install the following dependencies:

pnpm add @base-ui/react @stylexjs/stylex clsx

Set up the token layer and the StyleX compiler, if you have not already. See Installation. The avatar also needs interop.css, which sizes the icon inside a badge or a count.

Copy and paste the following code into your project.

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,
}

Update the import paths to match your project setup.

Usage

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
<Avatar>
  <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
  <AvatarFallback>CN</AvatarFallback>
</Avatar>

Composition

Avatar
├── AvatarImage
├── AvatarFallback
└── AvatarBadge
AvatarGroup
├── Avatar
└── AvatarGroupCount

Basic

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

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

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

PP
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>
  )
}

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.

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

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.

CNLRER
+3
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.

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

CNCNCN
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>
  )
}

Render a DropdownMenu trigger as a Button and put the avatar inside it.

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. Every part also takes xstyle and className.

Avatar

Renders a span.

PropTypeDefault
size"sm" | "default" | "lg"the group's, else "default"
xstyleStyleXStyles—
classNamestring—

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.

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

KeyWhat it is
baseThe circle, the hairline over the image
sizesOne entry per size
imageThe cover fit and the circle
fallbackThe flat fill behind initials
fallbackSizesThe fallback's type, keyed by size
badgeThe dot in the corner
badgeSizesThe dot's size and icon size, keyed by size
groupThe group's row
countThe tally's fill, hairline and shape
countSizesThe tally's size and type, keyed by size
memberThe overlap and ring a member in a group draws

Data attributes

AttributeOnEffect
data-slot="avatar"the rootMarks a member of a group
data-slot="avatar-group-count"the tallyMarks a member of a group
data-sizeevery partReflects the resolved size
data-slot="avatar-image"the imageTargets the image from a parent
data-slot="avatar-fallback"the fallbackTargets the fallback from a parent
data-slot="avatar-badge"the badgeTargets the badge from a parent
data-slot="avatar-group"the groupTargets 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

VariableOnEffect
--carbon-icon-sizebadge and tallyThe icon's size, read by interop.css