Avatar

An avatar stands for a person. It shows an image, falls back to initials when the image can't load, comes in three sizes, and can overlap with others in a group.

NSCNS
NSC
NSNS
import { Avatar, AvatarFallback, AvatarGroup, AvatarImage } from "@/components/ui/avatar";

// The photo is scaled up around the face so the circle shows the person, not the shoulders.
const face = "origin-[50%_38%] translate-y-[12%] scale-150 saturate-20";

export default function AvatarDemo() {
  return (
    <>
      <Avatar size="lg">
        <AvatarImage src="/assets/images/profile.webp" alt="Njoh Simplice Junior" className={face} />
        <AvatarFallback>NS</AvatarFallback>
      </Avatar>
      <Avatar size="lg">
        <AvatarImage src="/assets/images/claude-ai-icon.webp" alt="Claude" />
        <AvatarFallback>C</AvatarFallback>
      </Avatar>
      <Avatar size="lg">
        <AvatarImage src="/assets/images/missing.webp" alt="Njoh Simplice Junior" />
        <AvatarFallback>NS</AvatarFallback>
      </Avatar>
      <AvatarGroup size="lg" aria-label="Njoh Simplice Junior with Claude">
        <Avatar>
          <AvatarImage src="/assets/images/profile.webp" alt="" className={face} />
          <AvatarFallback>NS</AvatarFallback>
        </Avatar>
        <Avatar>
          <AvatarImage src="/assets/images/claude-ai-icon.webp" alt="" />
          <AvatarFallback>C</AvatarFallback>
        </Avatar>
      </AvatarGroup>
      <Avatar size="md">
        <AvatarImage src="/assets/images/profile.webp" alt="Njoh Simplice Junior" className={face} />
        <AvatarFallback>NS</AvatarFallback>
      </Avatar>
      <Avatar size="sm">
        <AvatarImage src="/assets/images/profile.webp" alt="Njoh Simplice Junior" className={face} />
        <AvatarFallback>NS</AvatarFallback>
      </Avatar>
    </>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

"use client";

import { Children, createContext, useContext, type ComponentProps } from "react";
import { Avatar as BaseAvatar } from "@base-ui/react/avatar";
import { cx } from "@/lib/cx";

type Size = "sm" | "md" | "lg";

// An avatar inside an AvatarGroup takes the group's size, so the circles always match.
const GroupSize = createContext<Size | undefined>(undefined);

const sizes: Record<Size, string> = {
  sm: "size-8 border-2",
  md: "size-12 border-2",
  lg: "size-14 border-3",
};

// How far each avatar tucks behind the one in front of it: about 70% of its width.
const overlap: Record<Size, string> = {
  sm: "-ml-5",
  md: "-ml-8",
  lg: "-ml-10",
};

export function Avatar({
  size,
  className,
  ...props
}: Omit<ComponentProps<typeof BaseAvatar.Root>, "className"> & { size?: Size; className?: string }) {
  const inherited = useContext(GroupSize);
  const resolved = size ?? inherited ?? "md";
  return (
    <BaseAvatar.Root
      className={cx(
        "relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border-text bg-surface align-middle text-text select-none",
        sizes[resolved],
        className,
      )}
      {...props}
    />
  );
}

export function AvatarImage({
  className,
  ...props
}: Omit<ComponentProps<typeof BaseAvatar.Image>, "className"> & { className?: string }) {
  return <BaseAvatar.Image className={cx("size-full object-cover", className)} {...props} />;
}

// Shown while the image loads, and instead of it when it fails.
export function AvatarFallback({
  className,
  ...props
}: Omit<ComponentProps<typeof BaseAvatar.Fallback>, "className"> & { className?: string }) {
  return (
    <BaseAvatar.Fallback
      className={cx("flex size-full items-center justify-center text-sm font-semibold", className)}
      {...props}
    />
  );
}

// Overlapping avatars. The first one is in front and each later one sits behind it.
export function AvatarGroup({
  size = "md",
  className,
  children,
  ...props
}: ComponentProps<"div"> & { size?: Size }) {
  const items = Children.toArray(children);
  return (
    <GroupSize.Provider value={size}>
      <div role="group" className={cx("inline-flex items-center", className)} {...props}>
        {items.map((child, index) => (
          <span
            key={index}
            style={{ zIndex: items.length - index }}
            className={cx("relative inline-flex", index > 0 && overlap[size])}
          >
            {child}
          </span>
        ))}
      </div>
    </GroupSize.Provider>
  );
}

Props

PropTypeDefaultWhat it does
sizesm | md | lgmdThe diameter: 32, 48 or 56px. On AvatarGroup it sets the size of every avatar inside.
srcstring—On AvatarImage. The picture to show.
altstring—On AvatarImage. The person's name, or an empty string when the name is written next to the avatar.
childrennode—On AvatarFallback. What to show when there is no image, usually initials.
classNamestring—Extra classes. The demo puts one on the image to crop in on the face.

Avatar, AvatarImage and AvatarFallback pass every other prop to the matching Base UI part. AvatarGroup is a plain div.

The thinking

An avatar is recognition at a glance, so something has to show at every moment: the image when it is ready, the initials when it is not.

  • Built on Base UI: The image is only drawn once it has loaded, and the fallback takes over if it fails, so a broken-image icon never appears.
  • Crop to the face: A square photo shows shoulders in a small circle. The demo scales the photo up around the face with a class on the image.
  • Groups have an order: The first avatar is in front and each later one tucks behind it, the way the home page shows the two of us.

Interface design

  • Sizes: 32, 48 and 56px circles. The border is 2px, or 3px at the large size, in the main text color.
  • Overlap in a group: Each avatar sits behind the one before it by about 70% of its width: 20, 32 or 40px.
  • Fallback: Initials in 14px semibold on the surface color.
  • Image: Fills the circle and is cropped to fit.

Interaction

  • No hover or press: An avatar is not a control. To make one open a profile, wrap it in a link or a button, and that element gets the focus ring.
  • Loading and failure: While the image loads, and if it never does, the fallback is shown. The third avatar in the demo points at a missing file to show it.
  • Stacking order: In a group the first child is always on top, however many avatars there are.

Empathy

  • Say who it is: Give each image an alt with the person's name, and label a group too, as the demo does.
  • Don't say it twice: If the name is written next to the avatar, use alt="" so a screen reader doesn't read it again. The two avatars inside the group do this, because the group is already labelled.
  • Never only a photo: Fallback initials mean the person is still identified when the image is slow or missing. They have 14.2:1 contrast on the surface color.