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 · lightui-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";