docs / core / avatar

Avatar

a person, as an image or their initials.

intensity · recedesrsc · client

provenance

shaped from sisu-plus

layer

core / primitives

intensity

recedes · safe anywhere, including dense surfaces

composition

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 person

a11y

alt names the image · the status dot is aria-hiddenjest-axe

dependencies

@base-ui/react · class-variance-authority
live 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
usagecopy
import { Avatar } from "usva/primitives/avatar";

<Avatar src="/jane.png" alt="Jane Doe" fallback="JD" status="online" />

props

proptypedefaultnotes
srcstringimage source. omit it and the fallback shows.
altstringthe person's name. required, it is the accessible name of the image.
fallbackstringinitials 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.jsoncopy
source · components/ui/avatar.tsxexactly what this command copiescopy
"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 copiescopy
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";