docs / core / avatar
Avatar
a person, as an image or their initials.
intensity · recedesrsc · client
provenance
shaped from sisu-pluslayer
core / primitivesintensity
recedes · safe anywhere, including dense surfacescomposition
sits in EntityCard, comment rows, nav account cornersstacks into AvatarGroup for social proofnever a click target on its own. wrap it in a link or buttonnot for logos or icons. it is a persona11y
alt names the image · the status dot is aria-hiddenjest-axedependencies
@base-ui/react · class-variance-authoritylive demo · try it out
JD
customize
sizediameter and initials size
tonefallback fill
statuspresence dot in the corner
imageoff shows the fallback initials
altthe person's accessible name
fallbackinitials shown without an image
usage
import { Avatar } from "usva/primitives/avatar";
<Avatar src="/jane.png" alt="Jane Doe" fallback="JD" status="online" />props
| prop | type | default | notes |
|---|---|---|---|
| src | string | — | image source. omit it and the fallback shows. |
| alt | string | — | the person's name. required, it is the accessible name of the image. |
| fallback | string | — | initials shown while the image loads, errors, or is missing. |
| size | "sm" | "md" | "lg" | "md" | diameter and initials size. |
| status | "online" | "away" | "busy" | "offline" | — | a presence dot in the corner. online pulses. |
| tone | "solid" | "accent" | "neutral" | "solid" | fallback fill: solid accent, tinted accent, or neutral surface. |
get it
npx shadcn add https://usva.build/r/avatar.jsonsource · components/ui/avatar.tsxexactly what this command copies
"use client";
import { Avatar as Base } from "@base-ui/react/avatar";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const rootVariants = cva(
"relative inline-flex shrink-0 select-none items-center justify-center rounded-full ring-1 ring-inset ring-border",
{
variants: {
size: {
sm: "h-8 w-8",
md: "h-10 w-10",
lg: "h-14 w-14",
},
},
defaultVariants: { size: "md" },
},
);
const fallbackVariants = cva(
"flex h-full w-full items-center justify-center font-semibold",
{
variants: {
size: {
sm: "text-xs",
md: "text-sm",
lg: "text-base",
},
tone: {
solid: "bg-accent-2 text-on-accent",
accent: "bg-accent-tint font-bold text-on-tint",
neutral: "bg-surface-2 text-muted",
},
},
defaultVariants: { size: "md", tone: "solid" },
},
);
export type AvatarTone = "solid" | "accent" | "neutral";
const dotVariants = cva(
"absolute right-0 bottom-0 rounded-full ring-2 ring-surface",
{
variants: {
size: {
sm: "h-2 w-2",
md: "h-2.5 w-2.5",
lg: "h-3.5 w-3.5",
},
status: {
online: "bg-live animate-live-pulse",
away: "bg-warning",
busy: "bg-danger",
offline: "bg-border-strong",
},
},
defaultVariants: { size: "md", status: "offline" },
},
);
export type AvatarStatus = "online" | "away" | "busy" | "offline";
export interface AvatarProps
extends Omit<
React.ComponentPropsWithoutRef<typeof Base.Root>,
"render" | "className"
>,
VariantProps<typeof rootVariants> {
className?: string;
src?: string;
alt: string;
fallback?: string;
status?: AvatarStatus;
/** Fallback fill: solid accent (default), tinted accent, or neutral. */
tone?: AvatarTone;
}
export const Avatar = React.forwardRef<HTMLSpanElement, AvatarProps>(
({ className, src, alt, fallback, size, status, tone, ...props }, ref) => {
return (
<Base.Root
ref={ref}
className={cn(rootVariants({ size }), className)}
{...props}
>
<span className="flex h-full w-full items-center justify-center overflow-hidden rounded-full bg-surface-2">
{src ? (
<Base.Image
src={src}
alt={alt}
className="h-full w-full object-cover"
/>
) : null}
<Base.Fallback className={fallbackVariants({ size, tone })}>
{fallback}
</Base.Fallback>
</span>
{status ? (
<span aria-hidden="true" className={dotVariants({ size, status })} />
) : null}
</Base.Root>
);
},
);
Avatar.displayName = "Avatar";
source · components/ui/avatar-group.tsxexactly what this command copies
import * as React from "react";
import { cn } from "@/lib/utils";
export type AvatarGroupSize = "sm" | "md" | "lg";
export type AvatarGroupTone = "solid" | "accent" | "neutral";
export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
/** Cap the visible avatars; the rest collapse into a "+N" chip. */
max?: number;
size?: AvatarGroupSize;
/** Caption after the stack, e.g. "25+ active users". */
label?: React.ReactNode;
/** Colors the overflow chip to match a tinted cluster. Defaults to neutral. */
tone?: AvatarGroupTone;
}
const overlap: Record<AvatarGroupSize, string> = {
sm: "-ml-1.5",
md: "-ml-2",
lg: "-ml-2.5",
};
const overflowChip: Record<AvatarGroupSize, string> = {
sm: "size-8 text-[0.7rem]",
md: "size-10 text-xs",
lg: "size-14 text-sm",
};
const chipTone: Record<AvatarGroupTone, string> = {
solid: "bg-accent-2 text-on-accent",
accent: "bg-accent-tint text-on-tint",
neutral: "bg-surface-2 text-muted",
};
export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
(
{
className,
max,
size = "md",
label,
tone = "neutral",
children,
...props
},
ref,
) => {
const all = React.Children.toArray(children);
const shown = max != null ? all.slice(0, max) : all;
const overflow = all.length - shown.length;
return (
<div ref={ref} className={cn("flex items-center", className)} {...props}>
<div className="flex items-center">
{shown.map((child, i) => (
<span
key={(child as React.ReactElement).key}
className={cn(
"rounded-full shadow-raised ring-2 ring-surface",
i > 0 && overlap[size],
)}
>
{child}
</span>
))}
{overflow > 0 && (
<span
className={cn(
"inline-flex items-center justify-center rounded-full font-bold tabular-nums shadow-raised ring-2 ring-surface",
overlap[size],
overflowChip[size],
chipTone[tone],
)}
>
+{overflow}
</span>
)}
</div>
{label != null && (
<span className="ml-3 text-sm font-medium text-muted">{label}</span>
)}
</div>
);
},
);
AvatarGroup.displayName = "AvatarGroup";