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
NSNSimport { 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
| Prop | Type | Default | What it does |
|---|---|---|---|
size | sm | md | lg | md | The diameter: 32, 48 or 56px. On AvatarGroup it sets the size of every avatar inside. |
src | string | — | On AvatarImage. The picture to show. |
alt | string | — | On AvatarImage. The person's name, or an empty string when the name is written next to the avatar. |
children | node | — | On AvatarFallback. What to show when there is no image, usually initials. |
className | string | — | 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.