shadimage
usva
uiavatarsource: ok · hit

Avatar

npx shadcn add "https://usva.build/r/avatar.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@base-ui/react

Files2 · source cached 10/9/2026, 3:30:03 PM

avatar.tsx
"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";
avatar-group.tsx
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";

More from usva

Browse all components in this library →