Agents (llms.txt)
Octocat

Avatar

A visual representation of a user that displays initials when an image is unavailable

PB
import { Avatar } from '@/components/avatar';

export default function AvatarPreview() {
  return (
    <Avatar>
      <Avatar.Image src="https://github.com/pdrbrnd.png" />
      <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
    </Avatar>
  );
}

Source Code

import { UserIcon } from '@phosphor-icons/react/dist/ssr';
import type { VariantProps } from 'cva';
import { Children, cloneElement, isValidElement, useId } from 'react';
import { cn, cva } from '@/lib/utils/classnames';

const getInitials = (name: string | undefined) => {
  if (!name) return '';

  if (name.length === 1 || name.length === 2) return name;

  return name
    .split(' ')
    .map((n) => n[0])
    .join('');
};

const avatarStyle = cva({
  base: 'relative flex items-center justify-center overflow-hidden bg-foreground-secondary/10 font-semibold text-foreground/80 shadow-[inset_0_0_0_1px_--alpha(var(--color-foreground)/8%)] backdrop-blur-sm',
  variants: {
    variant: {
      circle: 'rounded-full',
      square: 'rounded-md',
    },
    size: {
      '2xs': 'size-4 text-2xs',
      xs: 'size-6 text-2xs',
      sm: 'size-8 text-xs',
      md: 'size-10 text-sm',
      lg: 'size-12 text-base',
      xl: 'size-14 text-lg',
      '2xl': 'size-16 text-xl',
      '3xl': 'size-20 text-3xl',
    },
  },
});

interface AvatarProps extends React.ComponentPropsWithRef<'div'> {
  size?: VariantProps<typeof avatarStyle>['size'];
  variant?: VariantProps<typeof avatarStyle>['variant'];
}

const Avatar = ({
  className,
  variant = 'circle',
  size = 'md',
  children,
  ...props
}: AvatarProps) => {
  return (
    <div className={cn(avatarStyle({ variant, size }), className)} {...props}>
      {children}
    </div>
  );
};

interface AvatarImageProps extends React.ComponentPropsWithRef<'img'> {
  src: string;
}

const AvatarImage = ({ className, src, ...props }: AvatarImageProps) => {
  return (
    <img
      className={cn('absolute inset-0 z-1 object-cover', className)}
      src={src}
      alt=""
      {...props}
    />
  );
};

interface AvatarFallbackProps extends React.ComponentPropsWithRef<'div'> {
  children?: string;
}

const AvatarFallback = ({
  className,
  children,
  ...props
}: AvatarFallbackProps) => {
  return (
    <div className={cn('opacity-80', className)} {...props}>
      {getInitials(children) || <UserIcon weight="bold" />}
    </div>
  );
};

// Tailwind spacing units for each Avatar size variant; `2xs` (size-4) is 4 spacing units, etc.
// Kept in sync with `avatarStyle.variants.size` via the `satisfies` constraint below.
const SIZE_MAP = {
  '2xs': 4,
  xs: 6,
  sm: 8,
  md: 10,
  lg: 12,
  xl: 14,
  '2xl': 16,
  '3xl': 20,
} as const satisfies Record<NonNullable<AvatarProps['size']>, number>;

const OVERLAP_UNITS = 2;
const CUTOUT_GAP_UNITS = 1;
const SQUARE_RADIUS_UNITS = 1.5;

const toPathNumber = (value: number) => {
  return Number(value.toFixed(4));
};

const createCirclePath = ({
  cx,
  cy,
  radius,
}: {
  cx: number;
  cy: number;
  radius: number;
}) => {
  const left = toPathNumber(cx - radius);
  const right = toPathNumber(cx + radius);
  const centerY = toPathNumber(cy);
  const pathRadius = toPathNumber(radius);

  return [
    `M ${left} ${centerY}`,
    `A ${pathRadius} ${pathRadius} 0 1 0 ${right} ${centerY}`,
    `A ${pathRadius} ${pathRadius} 0 1 0 ${left} ${centerY}`,
    'Z',
  ].join(' ');
};

const createRoundedRectPath = ({
  x,
  y,
  width,
  height,
  radius,
}: {
  x: number;
  y: number;
  width: number;
  height: number;
  radius: number;
}) => {
  const right = x + width;
  const bottom = y + height;
  const pathRadius = Math.min(radius, width / 2, height / 2);

  return [
    `M ${toPathNumber(x + pathRadius)} ${toPathNumber(y)}`,
    `H ${toPathNumber(right - pathRadius)}`,
    `A ${toPathNumber(pathRadius)} ${toPathNumber(pathRadius)} 0 0 1 ${toPathNumber(right)} ${toPathNumber(y + pathRadius)}`,
    `V ${toPathNumber(bottom - pathRadius)}`,
    `A ${toPathNumber(pathRadius)} ${toPathNumber(pathRadius)} 0 0 1 ${toPathNumber(right - pathRadius)} ${toPathNumber(bottom)}`,
    `H ${toPathNumber(x + pathRadius)}`,
    `A ${toPathNumber(pathRadius)} ${toPathNumber(pathRadius)} 0 0 1 ${toPathNumber(x)} ${toPathNumber(bottom - pathRadius)}`,
    `V ${toPathNumber(y + pathRadius)}`,
    `A ${toPathNumber(pathRadius)} ${toPathNumber(pathRadius)} 0 0 1 ${toPathNumber(x + pathRadius)} ${toPathNumber(y)}`,
    'Z',
  ].join(' ');
};

const createAvatarCutoutPath = ({
  previousVariant,
  previousSize,
  currentSize,
}: {
  previousVariant: NonNullable<AvatarProps['variant']>;
  previousSize: NonNullable<AvatarProps['size']>;
  currentSize: NonNullable<AvatarProps['size']>;
}) => {
  const previousUnits = SIZE_MAP[previousSize];
  const currentUnits = SIZE_MAP[currentSize];
  const gap = CUTOUT_GAP_UNITS / currentUnits;
  const previousWidth = previousUnits / currentUnits;
  const previousCenterX = (OVERLAP_UNITS - previousUnits / 2) / currentUnits;
  const previousCenterY = 0.5;

  const cutout =
    previousVariant === 'circle'
      ? createCirclePath({
          cx: previousCenterX,
          cy: previousCenterY,
          radius: previousWidth / 2 + gap,
        })
      : createRoundedRectPath({
          x: (OVERLAP_UNITS - previousUnits - CUTOUT_GAP_UNITS) / currentUnits,
          y: (currentUnits - previousUnits) / 2 / currentUnits - gap,
          width: (previousUnits + CUTOUT_GAP_UNITS * 2) / currentUnits,
          height: (previousUnits + CUTOUT_GAP_UNITS * 2) / currentUnits,
          radius: (SQUARE_RADIUS_UNITS + CUTOUT_GAP_UNITS) / currentUnits,
        });

  return `M 0 0 H 1 V 1 H 0 Z ${cutout}`;
};

const cloneAvatarForGroup = (child: React.ReactElement<AvatarProps>) => {
  return cloneElement(child, {
    className: cn(child.props.className, 'backdrop-blur-none'),
  });
};

type AvatarGroupProps = React.ComponentPropsWithRef<'div'>;

const AvatarGroup = ({ children, className, ...props }: AvatarGroupProps) => {
  const id = useId();
  const items = Children.toArray(children).filter(
    (child): child is React.ReactElement<AvatarProps> =>
      isValidElement(child) && child.type === Avatar
  );

  return (
    <div
      className={cn(
        'isolate flex items-center -space-x-(--overlap) [--overlap:--spacing(2)]',
        className
      )}
      {...props}
    >
      {items.map((child, i) => {
        const key = child.key ?? i;
        const previous = items[i - 1];

        if (!previous) return cloneAvatarForGroup(child);

        const clipPathId = `${id}-${i}`;
        const previousVariant = previous.props.variant ?? 'circle';
        const previousSize = previous.props.size ?? 'md';
        const currentSize = child.props.size ?? 'md';
        const cutoutPath = createAvatarCutoutPath({
          currentSize,
          previousSize,
          previousVariant,
        });

        return (
          <div
            key={key}
            className="relative"
            style={{
              clipPath: `url(#${clipPathId})`,
              WebkitClipPath: `url(#${clipPathId})`,
            }}
          >
            <svg
              width="0"
              height="0"
              className="absolute size-0 overflow-hidden"
              aria-hidden="true"
              focusable="false"
            >
              <clipPath id={clipPathId} clipPathUnits="objectBoundingBox">
                <path clipRule="evenodd" fillRule="evenodd" d={cutoutPath} />
              </clipPath>
            </svg>

            {cloneAvatarForGroup(child)}
          </div>
        );
      })}
    </div>
  );
};

const CompoundAvatar = Object.assign(Avatar, {
  Image: AvatarImage,
  Fallback: AvatarFallback,
  Group: AvatarGroup,
});

export { CompoundAvatar as Avatar };

Anatomy


          <Avatar>
  <Avatar.Image />
  <Avatar.Fallback />
</Avatar>
        

API Reference

Avatar

Extends the div element.

Prop Default Type
size "md" "2xs""xs""sm""md""lg""xl""2xl""3xl"
variant "circle" "circle""square"

Avatar.Image

Extends the img element.

Prop Default Type
src - string

Avatar.Fallback

Extends the div element.

Prop Default Type
children - string

Avatar.Group

Renders direct <Avatar> children in an overlapping stack. Each non-leading avatar is cut by an SVG mask that follows the previous avatar’s variant (circle / square) and size, leaving a transparent gap. Children that aren’t <Avatar> are filtered out — wrap them only at the leaf level.

Extends the div element. No custom props.

Examples

Simple

PB
import { Avatar } from '@/components/avatar';

export default function AvatarPreview() {
  return (
    <Avatar>
      <Avatar.Image src="https://github.com/pdrbrnd.png" />
      <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
    </Avatar>
  );
}

Fallback

PB
S
PB
import { Avatar } from '@/components/avatar';

export default function AvatarFallbackPreview() {
  return (
    <div className="flex flex-wrap gap-2">
      {/* Full name */}
      <Avatar>
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      {/* One word */}
      <Avatar>
        <Avatar.Fallback>Significa</Avatar.Fallback>
      </Avatar>
      {/* Initials */}
      <Avatar>
        <Avatar.Fallback>PB</Avatar.Fallback>
      </Avatar>
      {/* No fallback */}
      <Avatar>
        <Avatar.Fallback />
      </Avatar>
    </div>
  );
}

Sizes

PB
PB
PB
PB
PB
PB
PB
PB
import { Avatar } from '@/components/avatar';

export default function AvatarSizesPreview() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Avatar size="2xs">
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar size="xs">
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar size="sm">
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar size="md">
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar size="lg">
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar size="xl">
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar size="2xl">
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar size="3xl">
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
    </div>
  );
}

Avatar Group

PB
PB
PB
import { Avatar } from '@/components/avatar';

export default function AvatarGroupPreview() {
  return (
    <Avatar.Group>
      <Avatar>
        <Avatar.Image src="https://github.com/pdrbrnd.png" />
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar size="2xl">
        <Avatar.Image src="https://github.com/pdrbrnd.png" />
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar size="md">
        <Avatar.Image src="https://github.com/pdrbrnd.png" />
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
    </Avatar.Group>
  );
}

Avatar Group (square)

PB
PB
PB
import { Avatar } from '@/components/avatar';

export default function AvatarGroupSquarePreview() {
  return (
    <Avatar.Group>
      <Avatar variant="square">
        <Avatar.Image src="https://github.com/pdrbrnd.png" />
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar variant="square" size="2xl">
        <Avatar.Image src="https://github.com/pdrbrnd.png" />
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
      <Avatar variant="square" size="md">
        <Avatar.Image src="https://github.com/pdrbrnd.png" />
        <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
      </Avatar>
    </Avatar.Group>
  );
}

Broken Image

PB
import { Avatar } from '@/components/avatar';

export default function AvatarBrokenImagePreview() {
  return (
    <Avatar>
      <Avatar.Image src="broken-image-url" />
      <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
    </Avatar>
  );
}

On top of media

PB
import { Avatar } from '@/components/avatar';

export default function AvatarOnTopOfMediaPreview() {
  return (
    <div
      className="relative size-32 overflow-hidden rounded-lg bg-center bg-cover"
      style={{
        backgroundImage:
          'url(https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?q=80)',
      }}
    >
      <div className="flex h-full items-center justify-center">
        <Avatar>
          <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
        </Avatar>
      </div>
    </div>
  );
}

Custom color

PB
import { Avatar } from '@/components/avatar';

export default function AvatarCustomColorPreview() {
  return (
    <Avatar className="bg-emerald-500/20">
      <Avatar.Fallback>Pedro Brandão</Avatar.Fallback>
    </Avatar>
  );
}

Previous

Setup

Next

Badge