VibeUI
ComponentsDocs
VibeUI

Components with taste, that you own. Copy the source, retint the tokens, keep every line.

Library

  • All components
  • Signature pieces
  • Text effects
  • Footers

Docs

  • Introduction
  • Installation
  • Theming
  • CLI

Project

  • GitHub
  • Agents and MCP
  • Issues
  • MIT license
© 2026 VibeUI · MIT licensed
  1. Components
  2. Data & surfaces
  3. Avatar

Avatar

Round avatar with image, graceful initials fallback and a stackable group.

Data & surfacesServer-safe2 exports
ALGHATMH
MKRSALSI+2

Install

bash
npx @agents-npm-packages/vibeui add avatar --dir ./src/components/ui

Import

tsx
import { Avatar, AvatarGroup } from "@/components/ui/avatar"

API

Generated from the component's TypeScript types, so it always matches the source. * marks a required prop.

<Avatar>

PropTypeDefaultDescription
fallback*string—Text used for initials when there is no image.
srcstring—Image URL.
altstring""Alternative text for the image.
size"xs" | "sm" | "md" | "lg" | "xl""md"Size preset.
classNamestring—Extra classes, merged with the component's own.

<AvatarGroup>

PropTypeDefaultDescription
children*ReactNode—Content rendered inside the component.
maxnumber—Upper bound.
classNamestring—Extra classes, merged with the component's own.

Source

src/components/ui/avatar.tsx
import * as React from "react";
import { cn } from "@/lib/utils";

const sizes = {
  xs: "size-6 text-[9px]",
  sm: "size-8 text-[10px]",
  md: "size-10 text-xs",
  lg: "size-12 text-sm",
  xl: "size-16 text-lg",
} as const;

function initials(text: string) {
  const parts = text.trim().split(/\s+/).filter(Boolean);
  if (parts.length > 1) return (parts[0][0] + parts[1][0]).toUpperCase();
  return text.slice(0, 2).toUpperCase();
}

/** Round avatar with image, graceful initials fallback and a stackable group. */
export function Avatar({
  src,
  alt = "",
  fallback,
  size = "md",
  className,
}: {
  src?: string;
  alt?: string;
  fallback: string;
  size?: keyof typeof sizes;
  className?: string;
}) {
  const [failed, setFailed] = React.useState(false);

  return (
    <span
      className={cn(
        "relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-surface-muted font-medium text-ink-soft ring-1 ring-inset ring-line-strong transition-transform duration-300 ease-spring hover:z-10 hover:-translate-y-0.5 hover:scale-110",
        sizes[size],
        className,
      )}
    >
      {src && !failed ? (
        // eslint-disable-next-line @next/next/no-img-element
        <img
          src={src}
          alt={alt}
          className="size-full object-cover"
          onError={() => setFailed(true)}
        />
      ) : (
        <span aria-hidden>{initials(fallback)}</span>
      )}
    </span>
  );
}

export function AvatarGroup({
  children,
  max,
  className,
}: {
  children: React.ReactNode;
  max?: number;
  className?: string;
}) {
  const all = React.Children.toArray(children);
  const shown = max ? all.slice(0, max) : all;
  const extra = max ? all.length - shown.length : 0;

  return (
    <div className={cn("flex items-center -space-x-2", className)}>
      {shown.map((child, i) =>
        React.isValidElement<{ className?: string }>(child)
          ? React.cloneElement(child, {
              key: child.key ?? i,
              className: cn("border-2 border-surface ring-0", child.props.className),
            })
          : child,
      )}
      {extra > 0 ? (
        <span className="relative inline-flex size-8 items-center justify-center rounded-full border-2 border-surface bg-surface-muted text-[10px] font-medium text-muted">
          +{extra}
        </span>
      ) : null}
    </div>
  );
}

Exports Avatar and 1 more. Peer dependencies: clsx, tailwind-merge.

PreviousAspect RatioNextBar Chart