Avatar
A radix-based avatar with 5 sizes and 2 shapes. AvatarFallback renders while the image loads or if it fails.
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/components/ui/avatar"
export default function AvatarDemo() {
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-3">
<Avatar>
<AvatarImage src="https://ai2.design/avatars/ai2-01.svg" alt="ai2" />
<AvatarFallback>A2</AvatarFallback>
</Avatar>
<Avatar size="lg" shape="rounded">
<AvatarFallback>AI</AvatarFallback>
</Avatar>
</div>
<AvatarGroup max={3}>
<Avatar>
<AvatarImage src="https://ai2.design/avatars/ai2-01.svg" alt="ai2" />
<AvatarFallback>A2</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="https://ai2.design/avatars/ai2-02.svg" alt="ai2" />
<AvatarFallback>AI</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>B2</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>K9</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback>M4</AvatarFallback>
</Avatar>
</AvatarGroup>
</div>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/avatarDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1 radix-ui@^1.6.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/avatar.tsx"use client"
import * as React from "react"
import { Avatar as AvatarPrimitive } from "@/components/ui/primitives"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const avatarVariants = cva(
"relative flex shrink-0 overflow-hidden bg-surface-3",
{
variants: {
size: {
xs: "size-6 text-[0.6rem]",
sm: "size-8 text-xs",
md: "size-10 text-sm",
lg: "size-12 text-base",
xl: "size-16 text-lg",
},
shape: {
circle: "rounded-full",
rounded: "rounded-lg",
},
},
defaultVariants: { size: "md", shape: "circle" },
}
)
interface AvatarProps
extends React.ComponentProps<typeof AvatarPrimitive.Root>,
VariantProps<typeof avatarVariants> {}
function Avatar({ className, size, shape, ...props }: AvatarProps) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(avatarVariants({ size, shape, className }))}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full object-cover", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"flex size-full items-center justify-center font-medium text-muted-foreground",
className
)}
{...props}
/>
)
}
interface AvatarGroupProps extends React.ComponentProps<"div"> {
/** Cap the number of avatars shown; the rest collapse into a +N tile. */
max?: number
}
function AvatarGroup({ className, max, children, ...props }: AvatarGroupProps) {
const items = React.Children.toArray(children)
const visible = max != null ? items.slice(0, max) : items
const overflow = max != null ? Math.max(items.length - max, 0) : 0
return (
<div
data-slot="avatar-group"
className={cn(
"flex -space-x-2 [&_[data-slot=avatar]]:ring-2 [&_[data-slot=avatar]]:ring-background",
className
)}
{...props}
>
{visible}
{overflow > 0 && (
<span
data-slot="avatar-group-overflow"
className="relative flex size-10 shrink-0 items-center justify-center rounded-full bg-surface-3 text-sm font-medium text-muted-foreground ring-2 ring-background"
>
+{overflow}
</span>
)}
</div>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarGroup,
avatarVariants,
type AvatarProps,
type AvatarGroupProps,
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the surface colors will be missing.
Usage
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
<Avatar size="lg" shape="rounded">
<AvatarImage src="https://ai2.design/avatars/ai2-01.svg" alt="ai2" />
<AvatarFallback>A2</AvatarFallback>
</Avatar>Put AvatarImage and AvatarFallback inside Avatar. The fallback shows while the image loads and stays if it fails.
Examples
Sizes
Shapes
Fallback
The image here is intentionally broken, so AvatarFallback takes over without any extra code.
Avatar group
Wrap several avatars in AvatarGroup to lay them out as an overlapping stack. Each avatar gets a ring in the background color so the overlaps stay crisp in both themes.
Avatar group with max
Set max to cap how many avatars appear; the extras collapse into a +N tile that closes the stack. This group holds five avatars with max={3}.
Props
The axes live on the root Avatar. All three parts also accept their radix primitive props (e.g. onLoadingStatusChange on AvatarImage, delayMs on AvatarFallback).
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Avatar dimensions; fallback text is scaled to match automatically. |
shape | "circle" | "rounded" | "circle" | Fully round or rounded-square silhouette. |
AvatarGroup wraps a set of avatars and accepts the standard div props plus one axis of its own.
| Prop | Type | Default | Description |
|---|---|---|---|
max | number | undefined | Cap the number of avatars shown; the remainder collapse into a +N tile. Omit to render every child. |
ai2 Avatar: radix avatar with graceful fallback for React
The ai2 Avatar is a shadcn-compatible react avatar component, built on the radix-ui Avatar primitive and styled with Tailwind CSS v4. It shows a user image with an automatic text fallback, in 5 sizes and 2 shapes, for navbars, comment threads, member lists and team stacks. The broken image problem is solved at the primitive level: the fallback renders while the image loads and stays if it never arrives.
It ships through the shadcn registry format, so you install it with one CLI command, an MCP agent, or a copy-paste, and the TypeScript source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Avatar?
It is a three-part composition: Avatar, AvatarImage and AvatarFallback. The anatomy matches shadcn/ui exactly, so existing snippets, muscle memory and AI agents keep working. On top, the root adds two cva axes: size (xs, sm, md, lg, xl) and shape (circle, rounded). A fourth export, AvatarGroup, stacks avatars into an overlapping facepile and, with a max prop, collapses the extras into a +N tile.
The radix primitive tracks the image loading state internally: AvatarFallback renders until the image has actually loaded, then swaps seamlessly, and comes back if the src errors. Fallback text scales with the size axis automatically (from 0.6rem at xs to lg text at xl), and the background comes from the ai2 surface tokens, so avatars sit correctly on both light and dark themes.
Why use it
- No broken-image icons, ever: The radix Avatar primitive only shows AvatarImage after it loads successfully. Slow networks, missing files and blocked trackers all degrade to your initials fallback instead of a broken glyph.
- Five sizes with matched typography: Each size pairs the box dimension with a fallback font size, so initials stay proportional from a 24px table avatar to a 64px profile header.
- Two shapes from one prop: shape="circle" for people, shape="rounded" for teams, workspaces and bots. One prop, no border-radius overrides.
- Token-driven surface: The placeholder background uses the ai2 surface-3 token, so empty avatars blend with your theme instead of showing a hardcoded gray.
- Agent-readable metadata: The registry item describes the parts and axes in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its radix dependency and the @ai2/tokens theme to your project.
- Automatic image fallback: AvatarFallback renders during loading and on error, driven by the radix loading-status machine, with zero code on your side.
- 5 sizes, 2 shapes: The cva axes cover xs (24px) through xl (64px) and circle or rounded silhouettes; the default is a medium circle.
- Radix props forwarded: AvatarImage accepts onLoadingStatusChange and AvatarFallback accepts delayMs, so you can delay the fallback to avoid a flash on fast connections.
- Data attributes for styling: Every part exposes data-slot (avatar, avatar-image, avatar-fallback), so you can restyle parts from CSS without forking the component.
- TypeScript source: The file you install is typed end to end, including the exported avatarVariants function for matching other elements to avatar sizing.
Production tips
- Always provide a fallback: Render AvatarFallback with initials (one or two characters) in every avatar. Without it, users see an empty circle while the image loads or after it fails.
- Set alt on AvatarImage: Use the person's name as alt text. When the image renders, that is what screen readers announce; when it fails, make sure the surrounding row also contains the visible name.
- Use delayMs to avoid fallback flash: On fast connections the fallback can flash for a frame before the image loads. Pass delayMs={600} to AvatarFallback so it only appears when loading is actually slow.
- Pick shape by entity type: A consistent convention (circles for humans, rounded squares for organizations and bots) lets users parse member lists faster than any label.
- Reach for AvatarGroup for facepiles: Instead of hand-rolling a flex row, wrap avatars in AvatarGroup: it applies -space-x-2 and a ring in the page background color so overlapping edges stay crisp in both themes, and its max prop caps the stack with a +N tile.
Works with the rest of ai2
Avatars anchor identity across the registry. Pair one with an ai2 Badge for presence or role labels, put it inside an ai2 Dropdown Menu trigger for the classic account menu, or wrap it in an ai2 Tooltip to reveal the full name in compact facepiles.
In content, avatars lead ai2 Card headers for author bylines and sit in ai2 Table member columns at the xs size. Everything shares one token source, so the placeholder surface matches the rest of your theme in both modes.