Avatars

Versatile avatar components for representing users, entities, or brands. Supports various shapes, sizes, styles, and features including initials, icons, logos, status indicators, badges, and customizable colors. previews

Circular Avatars

Round avatar images for a modern, friendly appearance.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Circular" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Circular" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Circular" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Circular" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Circular" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Circular" src="path/to/image.jpg">
    </div>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="Circular" shape="circular" size="xs" />
<Avatar src={sampleAvatar} alt="Circular" shape="circular" size="sm" />
<Avatar src={sampleAvatar} alt="Circular" shape="circular" size="md" />
<Avatar src={sampleAvatar} alt="Circular" shape="circular" size="lg" />
<Avatar src={sampleAvatar} alt="Circular" shape="circular" size="xl" />
<Avatar src={sampleAvatar} alt="Circular" shape="circular" size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Rounded Avatars

Softly rounded avatar images for a balanced look.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-lg size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-lg size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Rounded" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-lg size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-lg size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Rounded" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-lg size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-lg size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Rounded" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-lg size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-lg size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Rounded" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-lg size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-lg size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Rounded" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-lg size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-lg size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Rounded" src="path/to/image.jpg">
    </div>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="Rounded" shape="rounded" size="xs" />
<Avatar src={sampleAvatar} alt="Rounded" shape="rounded" size="sm" />
<Avatar src={sampleAvatar} alt="Rounded" shape="rounded" size="md" />
<Avatar src={sampleAvatar} alt="Rounded" shape="rounded" size="lg" />
<Avatar src={sampleAvatar} alt="Rounded" shape="rounded" size="xl" />
<Avatar src={sampleAvatar} alt="Rounded" shape="rounded" size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Square Avatars

Crisp, square-edged avatar images for a formal style.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-none size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-none size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Square" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-none size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-none size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Square" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-none size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-none size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Square" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-none size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-none size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Square" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-none size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-none size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Square" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-none size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-none size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Square" src="path/to/image.jpg">
    </div>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="Square" shape="square" size="xs" />
<Avatar src={sampleAvatar} alt="Square" shape="square" size="sm" />
<Avatar src={sampleAvatar} alt="Square" shape="square" size="md" />
<Avatar src={sampleAvatar} alt="Square" shape="square" size="lg" />
<Avatar src={sampleAvatar} alt="Square" shape="square" size="xl" />
<Avatar src={sampleAvatar} alt="Square" shape="square" size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Initials Avatars

Text-based avatars using initials when images are unavailable.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="font-medium">JD</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="font-medium">JD</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="font-medium">JD</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="font-medium">JD</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="font-medium">JD</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="font-medium">JD</span>
    </div>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';

---

<Avatar initials="JD" size="xs" />
<Avatar initials="JD" size="sm" />
<Avatar initials="JD" size="md" />
<Avatar initials="JD" size="lg" />
<Avatar initials="JD" size="xl" />
<Avatar initials="JD" size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Icon Avatars

Icon-based avatars for representing roles or categories.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-3">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-4">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-5">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-6">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-8">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-10">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';

---

<Avatar icon="UserIcon" size="xs" />
<Avatar icon="UserIcon" size="sm" />
<Avatar icon="UserIcon" size="md" />
<Avatar icon="UserIcon" size="lg" />
<Avatar icon="UserIcon" size="xl" />
<Avatar icon="UserIcon" size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Bordered Avatars

Avatars with a border for a subtle, outlined appearance.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-2 border-slate-600 dark:border-slate-500">
      <img width="400" height="400" alt="Bordered" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-2 border-purple-600 dark:border-purple-500">
      <img width="400" height="400" alt="Bordered" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-2 border-indigo-600 dark:border-indigo-500">
      <span class="font-medium">JD</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-2 border-green-600 dark:border-green-500">
      <span class="inline-block size-6">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-2 border-blue-600 dark:border-blue-500">
      <img width="400" height="400" alt="Bordered" src="path/to/image.jpg">
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-2 border-yellow-600 dark:border-yellow-500">
      <img width="400" height="400" alt="Bordered" src="path/to/image.jpg">
    </div>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="Bordered" bordered borderColor="slate" size="xs" />
<Avatar src={sampleAvatar} alt="Bordered" bordered borderColor="purple" size="sm" />
<Avatar initials="JD" bordered borderColor="indigo" size="md" />
<Avatar icon="UserIcon" bordered borderColor="green" size="lg" />
<Avatar src={sampleAvatar} alt="Bordered" bordered borderColor="blue" size="xl" />
<Avatar src={sampleAvatar} alt="Bordered" bordered borderColor="yellow" size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Status Avatars

Avatars with status indicators for online, offline, or busy status.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Online" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-2 h-2 bottom-[2%] right-[2%] bg-green-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Offline" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-2.5 h-2.5 bottom-[2%] right-[2%] bg-gray-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Busy" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-3 h-3 bottom-[2%] right-[2%] bg-red-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Online" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-3.5 h-3.5 bottom-[2%] right-[2%] bg-green-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Offline" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-4 h-4 bottom-[2%] right-[2%] bg-gray-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Busy" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-4 h-4 bottom-[2%] right-[2%] bg-red-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="Online" status="online" size="xs" />
<Avatar src={sampleAvatar} alt="Offline" status="offline" size="sm" />
<Avatar src={sampleAvatar} alt="Busy" status="busy" size="md" />
<Avatar src={sampleAvatar} alt="Online" status="online" size="lg" />
<Avatar src={sampleAvatar} alt="Offline" status="offline" size="xl" />
<Avatar src={sampleAvatar} alt="Busy" status="busy" size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Avatar Position

Customizable status indicator positions on avatars.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="XS Top Left" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-2 h-2 top-[2%] left-[2%] bg-green-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="SM Top Right" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-2.5 h-2.5 top-[2%] right-[2%] bg-gray-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="MD Bottom Left" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-3 h-3 bottom-[2%] left-[2%] bg-red-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="LG Bottom Right" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-3.5 h-3.5 bottom-[2%] right-[2%] bg-green-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="XL Top Left" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-4 h-4 top-[2%] left-[2%] bg-gray-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="2XL Bottom Right" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-4 h-4 bottom-[2%] right-[2%] bg-red-500 rounded-full border-2 border-white dark:border-gray-800"></span>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="XS Top Left" status="online" statusPosition="top-left" size="xs" />
<Avatar src={sampleAvatar} alt="SM Top Right" status="offline" statusPosition="top-right" size="sm" />
<Avatar src={sampleAvatar} alt="MD Bottom Left" status="busy" statusPosition="bottom-left" size="md" />
<Avatar src={sampleAvatar} alt="LG Bottom Right" status="online" statusPosition="bottom-right" size="lg" />
<Avatar src={sampleAvatar} alt="XL Top Left" status="offline" statusPosition="top-left" size="xl" />
<Avatar src={sampleAvatar} alt="2XL Bottom Right" status="busy" statusPosition="bottom-right" size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Shadow Avatars

Avatars with different levels of shadow for depth and emphasis.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="No Shadow" src="path/to/image.jpg">
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-8 text-sm shadow-sm dark:shadow-gray-700/50">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm shadow-sm dark:shadow-gray-700/50 overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="SM Shadow" src="path/to/image.jpg">
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base shadow-md dark:shadow-gray-700/50">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base shadow-md dark:shadow-gray-700/50 overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="MD Shadow" src="path/to/image.jpg">
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-14 text-lg shadow-lg dark:shadow-gray-600/50">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg shadow-lg dark:shadow-gray-600/50 overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="LG Shadow" src="path/to/image.jpg">
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-16 text-xl shadow-xl dark:shadow-gray-500/50">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl shadow-xl dark:shadow-gray-500/50 overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="XL Shadow" src="path/to/image.jpg">
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-20 text-2xl shadow-2xl dark:shadow-gray-400/50">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl shadow-2xl dark:shadow-gray-400/50 overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="2XL Shadow" src="path/to/image.jpg">
    </div>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="No Shadow" shadow="none" size="md" />
<Avatar src={sampleAvatar} alt="SM Shadow" shadow="sm" size="md" />
<Avatar src={sampleAvatar} alt="MD Shadow" shadow="md" size="md" />
<Avatar src={sampleAvatar} alt="LG Shadow" shadow="lg" size="md" />
<Avatar src={sampleAvatar} alt="XL Shadow" shadow="xl" size="md" />
<Avatar src={sampleAvatar} alt="2XL Shadow" shadow="2xl" size="md" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Colored Initials Avatars

Initials avatars with customizable background colors.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="font-medium">JD</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-red-500 dark:bg-red-600 text-white dark:text-white hover:text-red-100 dark:hover:text-red-200 border-red-600 dark:border-red-500">
      <span class="font-medium">AB</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-yellow-500 dark:bg-yellow-600 text-white dark:text-white hover:text-yellow-100 dark:hover:text-yellow-200 border-yellow-600 dark:border-yellow-500">
      <span class="font-medium">CD</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-green-500 dark:bg-green-600 text-white dark:text-white hover:text-green-100 dark:hover:text-green-200 border-green-600 dark:border-green-500">
      <span class="font-medium">EF</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-blue-500 dark:bg-blue-600 text-white dark:text-white hover:text-blue-100 dark:hover:text-blue-200 border-blue-600 dark:border-blue-500">
      <span class="font-medium">GH</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-indigo-500 dark:bg-indigo-600 text-white dark:text-white hover:text-indigo-100 dark:hover:text-indigo-200 border-indigo-600 dark:border-indigo-500">
      <span class="font-medium">IJ</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-purple-500 dark:bg-purple-600 text-white dark:text-white hover:text-purple-100 dark:hover:text-purple-200 border-purple-600 dark:border-purple-500">
      <span class="font-medium">KL</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-pink-500 dark:bg-pink-600 text-white dark:text-white hover:text-pink-100 dark:hover:text-pink-200 border-pink-600 dark:border-pink-500">
      <span class="font-medium">MN</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-orange-500 dark:bg-orange-600 text-white dark:text-white hover:text-orange-100 dark:hover:text-orange-200 border-orange-600 dark:border-orange-500">
      <span class="font-medium">OP</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-teal-500 dark:bg-teal-600 text-white dark:text-white hover:text-teal-100 dark:hover:text-teal-200 border-teal-600 dark:border-teal-500">
      <span class="font-medium">QR</span>
    </div>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-cyan-500 dark:bg-cyan-600 text-white dark:text-white hover:text-cyan-100 dark:hover:text-cyan-200 border-cyan-600 dark:border-cyan-500">
      <span class="font-medium">ST</span>
    </div>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';

---

<Avatar initials="JD" color="gray" size="md" />
<Avatar initials="AB" color="red" size="md" />
<Avatar initials="CD" color="yellow" size="md" />
<Avatar initials="EF" color="green" size="md" />
<Avatar initials="GH" color="blue" size="md" />
<Avatar initials="IJ" color="indigo" size="md" />
<Avatar initials="KL" color="purple" size="md" />
<Avatar initials="MN" color="pink" size="md" />
<Avatar initials="OP" color="orange" size="md" />
<Avatar initials="QR" color="teal" size="md" />
<Avatar initials="ST" color="cyan" size="md" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Outlined Avatars

Avatars with an outlined border for a subtle, modern look.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 border border-slate-500 dark:border-slate-400 hover:border-slate-700 dark:hover:border-slate-200">
      <span class="font-medium">JD</span>
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-red-500 dark:text-red-400 hover:text-red-700 dark:hover:text-red-200 border border-red-500 dark:border-red-400 hover:border-red-700 dark:hover:border-red-200">
      <span class="font-medium">AB</span>
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-yellow-500 dark:text-yellow-400 hover:text-yellow-700 dark:hover:text-yellow-200 border border-yellow-500 dark:border-yellow-400 hover:border-yellow-700 dark:hover:border-yellow-200">
      <span class="font-medium">CD</span>
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-green-500 dark:text-green-400 hover:text-green-700 dark:hover:text-green-200 border border-green-500 dark:border-green-400 hover:border-green-700 dark:hover:border-green-200">
      <span class="font-medium">EF</span>
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-indigo-500 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-200 border border-indigo-500 dark:border-indigo-400 hover:border-indigo-700 dark:hover:border-indigo-200">
      <span class="font-medium">IJ</span>
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-purple-500 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-200 border border-purple-500 dark:border-purple-400 hover:border-purple-700 dark:hover:border-purple-200">
      <span class="font-medium">KL</span>
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-pink-500 dark:text-pink-400 hover:text-pink-700 dark:hover:text-pink-200 border border-pink-500 dark:border-pink-400 hover:border-pink-700 dark:hover:border-pink-200">
      <span class="font-medium">MN</span>
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-orange-500 dark:text-orange-400 hover:text-orange-700 dark:hover:text-orange-200 border border-orange-500 dark:border-orange-400 hover:border-orange-700 dark:hover:border-orange-200">
      <span class="font-medium">OP</span>
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-teal-500 dark:text-teal-400 hover:text-teal-700 dark:hover:text-teal-200 border border-teal-500 dark:border-teal-400 hover:border-teal-700 dark:hover:border-teal-200">
      <span class="font-medium">QR</span>
    </div>
  </div>
  <div
    class="relative flex items-center justify-center rounded-full size-12 text-base bg-transparent">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-transparent text-cyan-500 dark:text-cyan-400 hover:text-cyan-700 dark:hover:text-cyan-200 border border-cyan-500 dark:border-cyan-400 hover:border-cyan-700 dark:hover:border-cyan-200">
      <span class="font-medium">ST</span>
    </div>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';

---

<Avatar initials="JD" color="slate" variant="outline" size="md" />
<Avatar initials="AB" color="red" variant="outline" size="md" />
<Avatar initials="CD" color="yellow" variant="outline" size="md" />
<Avatar initials="EF" color="green" variant="outline" size="md" />
<Avatar initials="IJ" color="indigo" variant="outline" size="md" />
<Avatar initials="KL" color="purple" variant="outline" size="md" />
<Avatar initials="MN" color="pink" variant="outline" size="md" />
<Avatar initials="OP" color="orange" variant="outline" size="md" />
<Avatar initials="QR" color="teal" variant="outline" size="md" />
<Avatar initials="ST" color="cyan" variant="outline" size="md" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Logo Avatars with Background

Avatars with a solid background and a logo in the center.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-3">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
    <span
      class="absolute -bottom-[8%] -right-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-4">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
    <span
      class="absolute -bottom-[8%] -right-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-5">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
    <span
      class="absolute -bottom-[8%] -right-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-6">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
    <span
      class="absolute -bottom-[8%] -right-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-8">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
    <span
      class="absolute -bottom-[8%] -right-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <span class="inline-block size-10">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="currentColor"
          aria-hidden="true"
          data-slot="icon">
          <path
            fill-rule="evenodd"
            d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
            clip-rule="evenodd"></path>
        </svg>
      </span>
    </div>
    <span
      class="absolute -bottom-[8%] -right-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';

---

<Avatar logo={tailwindLogo} alt="Tailwind Logo" size="xs" />
<Avatar logo={tailwindLogo} alt="Tailwind Logo" size="sm" />
<Avatar logo={tailwindLogo} alt="Tailwind Logo" size="md" />
<Avatar logo={tailwindLogo} alt="Tailwind Logo" size="lg" />
<Avatar logo={tailwindLogo} alt="Tailwind Logo" size="xl" />
<Avatar logo={tailwindLogo} alt="Tailwind Logo" size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Logo Avatars

Avatars with a background image and a logo overlay.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Tailwind Logo" src="path/to/image.jpg">
    </div>
    <span
      class="absolute -bottom-[8%] -right-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Tailwind Logo" src="path/to/image.jpg">
    </div>
    <span
      class="absolute -bottom-[8%] -left-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Tailwind Logo" src="path/to/image.jpg">
    </div>
    <span
      class="absolute -top-[8%] -right-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Tailwind Logo" src="path/to/image.jpg">
    </div>
    <span
      class="absolute -top-[8%] -left-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Tailwind Logo" src="path/to/image.jpg">
    </div>
    <span
      class="absolute -bottom-[8%] -right-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Tailwind Logo" src="path/to/image.jpg">
    </div>
    <span
      class="absolute -bottom-[8%] -left-[8%] w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm">
      <img width="160" height="160" alt="Company logo" src="path/to/image.jpg">
    </span>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} logo={tailwindLogo} alt="Tailwind Logo" size="xs" color="gray" />
<Avatar src={sampleAvatar} logo={tailwindLogo} alt="Tailwind Logo" size="sm" color="gray" />
<Avatar src={sampleAvatar} logo={tailwindLogo} alt="Tailwind Logo" size="md" color="gray" />
<Avatar src={sampleAvatar} logo={tailwindLogo} alt="Tailwind Logo" size="lg" color="gray" />
<Avatar src={sampleAvatar} logo={tailwindLogo} alt="Tailwind Logo" size="xl" color="gray" />
<Avatar src={sampleAvatar} logo={tailwindLogo} alt="Tailwind Logo" size="2xl" color="gray" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Badge Avatars

Avatars with badges for notifications or status indicators.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-6 text-xs">
    <div
      class="relative flex items-center justify-center rounded-full size-6 text-xs overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-3 h-3 text-[0.4rem] -top-[8%] -right-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      3
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-8 text-sm">
    <div
      class="relative flex items-center justify-center rounded-full size-8 text-sm overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-4 h-4 text-[0.5rem] -top-[8%] -right-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      7
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -top-[8%] -right-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      12
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-14 text-lg">
    <div
      class="relative flex items-center justify-center rounded-full size-14 text-lg overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-6 h-6 text-[0.7rem] -top-[8%] -right-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      50
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-16 text-xl">
    <div
      class="relative flex items-center justify-center rounded-full size-16 text-xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-7 h-7 text-[0.8rem] -top-[8%] -right-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      99
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-20 text-2xl">
    <div
      class="relative flex items-center justify-center rounded-full size-20 text-2xl overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-8 h-8 text-[0.9rem] -top-[8%] -right-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      100
    </span>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="Badge" badge={3} size="xs"  />
<Avatar src={sampleAvatar} alt="Badge" badge={7} size="sm" />
<Avatar src={sampleAvatar} alt="Badge" badge={12} size="md" />
<Avatar src={sampleAvatar} alt="Badge" badge={50} size="lg" />
<Avatar src={sampleAvatar} alt="Badge" badge={99} size="xl" />
<Avatar src={sampleAvatar} alt="Badge" badge={100} size="2xl" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Colored Badge Avatars

Avatars with badges in various colors for different statuses.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Red Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -top-[8%] -right-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      3
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Green Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -top-[8%] -right-[8%] bg-green-500 dark:bg-green-500 text-white dark:text-white hover:text-green-200 dark:hover:text-green-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      7
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Blue Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -top-[8%] -right-[8%] bg-blue-500 dark:bg-blue-500 text-white dark:text-white hover:text-blue-200 dark:hover:text-blue-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      12
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Yellow Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -top-[8%] -right-[8%] bg-yellow-500 dark:bg-yellow-500 text-white dark:text-white hover:text-yellow-200 dark:hover:text-yellow-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      50
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Purple Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -top-[8%] -right-[8%] bg-purple-500 dark:bg-purple-500 text-white dark:text-white hover:text-purple-200 dark:hover:text-purple-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      87
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Pink Badge" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -top-[8%] -right-[8%] bg-pink-500 dark:bg-pink-500 text-white dark:text-white hover:text-pink-200 dark:hover:text-pink-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      99
    </span>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="Red Badge" badge={3} badgeColor="red" size="md" />
<Avatar src={sampleAvatar} alt="Green Badge" badge={7} badgeColor="green" size="md" />
<Avatar src={sampleAvatar} alt="Blue Badge" badge={12} badgeColor="blue" size="md" />
<Avatar src={sampleAvatar} alt="Yellow Badge" badge={50} badgeColor="yellow" size="md" />
<Avatar src={sampleAvatar} alt="Purple Badge" badge={99} badgeColor="purple" size="md" />
<Avatar src={sampleAvatar} alt="Pink Badge" badge={100} badgeColor="pink" size="md" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Badge Positions

Customizable badge placements on avatars for notifications or status indicators.

 
<div
  class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&#38;>*]:z-0 [&#38;>*:hover]:z-10 w-fit mx-auto">
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Top Left" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -top-[8%] -left-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      1
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Top Right" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -top-[8%] -right-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      2
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Bottom Left" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -bottom-[8%] -left-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      3
    </span>
  </div>
  <div class="relative flex items-center justify-center rounded-full size-12 text-base">
    <div
      class="relative flex items-center justify-center rounded-full size-12 text-base overflow-hidden bg-gray-500 dark:bg-gray-600 text-white dark:text-white hover:text-gray-100 dark:hover:text-gray-200 border-gray-600 dark:border-gray-500">
      <img width="400" height="400" alt="Bottom Right" src="path/to/image.jpg">
    </div>
    <span
      class="absolute w-5 h-5 text-[0.6rem] -bottom-[8%] -right-[8%] bg-red-500 dark:bg-red-500 text-white dark:text-white hover:text-red-200 dark:hover:text-red-300 rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800">
      4
    </span>
  </div>
</div>
---
import { Avatar } from '@/components/ui/avatar';
import sampleAvatar from '@/images/sample-avatar.jpg';
---

<Avatar src={sampleAvatar} alt="Top Left" badge={1} badgePosition="top-left" size="md" />
<Avatar src={sampleAvatar} alt="Top Right" badge={2} badgePosition="top-right" size="md" />
<Avatar src={sampleAvatar} alt="Bottom Left" badge={3} badgePosition="bottom-left" size="md" />
<Avatar src={sampleAvatar} alt="Bottom Right" badge={4} badgePosition="bottom-right" size="md" />
---
// Imports
import { type HTMLAttributes } from "astro/types";
import { tv, type VariantProps } from "@utils/custom-tv";
import { Image } from "astro:assets";
import Icon from "./Icon.astro";
import {
  getSoftBgClass,
  getDefaultTextClass,
  getDefaultBorderClass,
  getBadgeClasses,
  getOutlinedClasses,
  type ColorName,
  type ColorIntensity,
  getDefaultClasses,
  getSoftClasses,
  colorPalette,
  getDefaultBorderClass,
} from "@utils/colorUtils";
import { twMerge } from "tailwind-merge";

// Types and Interfaces
type AvatarVariants = VariantProps<typeof avatarStyles>;

interface Props extends Omit<HTMLAttributes<"div">, "size">, AvatarVariants {
  src?: ImageMetadata;
  alt?: string;
  initials?: string;
  icon?: string;
  class?: string;
  bordered?: boolean;
  status?: "online" | "offline" | "away" | "busy" | "dnd" | "invisible";
  statusPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  shadow?: "sm" | "md" | "lg" | "xl" | "2xl" | "none";
  color?: ColorName;
  colorIntensity?: ColorIntensity;
  variant?: "filled" | "outline";
  shape?: "circular" | "rounded" | "square";
  size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
  badge?: number;
  badgePosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  logo?: ImageMetadata;
  logoPosition?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
  badgeColor?: ColorName;
  borderColor?: ColorName;
}

// Styles
const avatarStyles = tv({
  base: "relative w-full h-full flex items-center justify-center",
  variants: {
    shape: {
      circular: "rounded-full",
      rounded: "rounded-lg",
      square: "rounded-none",
    },
    size: {
      xs: "size-6 text-xs",
      sm: "size-8 text-sm",
      md: "size-12 text-base",
      lg: "size-14 text-lg",
      xl: "size-16 text-xl",
      "2xl": "size-20 text-2xl",
    },
    variant: {
      filled: "",
      outline: "bg-transparent",
    },
    shadow: {
      none: "",
      sm: "shadow-sm dark:shadow-gray-700/50",
      md: "shadow-md dark:shadow-gray-700/50",
      lg: "shadow-lg dark:shadow-gray-600/50",
      xl: "shadow-xl dark:shadow-gray-500/50",
      "2xl": "shadow-2xl dark:shadow-gray-400/50",
    },
  },
  defaultVariants: {
    shape: "circular",
    size: "md",
    variant: "filled",
    shadow: "none",
  },
});

// Helper Functions
const getIndicatorSize = (size: Props["size"], isStatus: boolean): string => {
  const sizes = {
    xs: isStatus ? "w-2 h-2" : "w-3 h-3 text-[0.4rem]",
    sm: isStatus ? "w-2.5 h-2.5" : "w-4 h-4 text-[0.5rem]",
    md: isStatus ? "w-3 h-3" : "w-5 h-5 text-[0.6rem]",
    lg: isStatus ? "w-3.5 h-3.5" : "w-6 h-6 text-[0.7rem]",
    xl: isStatus ? "w-4 h-4" : "w-7 h-7 text-[0.8rem]",
    "2xl": isStatus ? "w-4 h-4" : "w-8 h-8 text-[0.9rem]",
  };
  return sizes[size] || sizes.md;
};

const getIndicatorPosition = (
  position: string,
  size: Props["size"],
  isStatus: boolean,
): string => {
  if (isStatus) {
    const positions = {
      "bottom-right": `bottom-[2%] right-[2%]`,
      "bottom-left": `bottom-[2%] left-[2%]`,
      "top-right": `top-[2%] right-[2%]`,
      "top-left": `top-[2%] left-[2%]`,
    };
    return positions[position] || positions["bottom-right"];
  } else {
    const positions = {
      "bottom-right": `-bottom-[8%] -right-[8%]`,
      "bottom-left": `-bottom-[8%] -left-[8%]`,
      "top-right": `-top-[8%] -right-[8%]`,
      "top-left": `-top-[8%] -left-[8%]`,
    };
    return positions[position] || positions["bottom-right"];
  }
};

const getStatusColor = (status: Props["status"]): string => {
  const colors = {
    online: "bg-green-500",
    offline: "bg-gray-500",
    away: "bg-yellow-500",
    busy: "bg-red-500",
    dnd: "bg-red-500",
    invisible: "bg-gray-300",
  };
  return colors[status] || colors.offline;
};

// Component Logic
const {
  src,
  alt,
  initials,
  icon,
  shape,
  size = "md",
  bordered,
  status,
  statusPosition = "bottom-right",
  shadow = "none",
  color = "gray",
  colorIntensity = "default",
  variant = "filled",
  class: className = "",
  badge,
  badgePosition = "top-right",
  logo,
  logoPosition = "bottom-right",
  badgeColor = "red",
  borderColor,
} = Astro.props as Props;

const containerClasses = avatarStyles({
  shape,
  size,
  shadow,
  variant,
});

const contentClasses = twMerge(
  containerClasses,
  "overflow-hidden", // Add overflow-hidden here
  variant === "filled" ? getDefaultClasses(color) : getDefaultTextClass(color),
  variant === "outline"
    ? `border-2 ${getOutlinedClasses(color)}`
    : bordered
      ? `border-2 ${
          borderColor
            ? getDefaultBorderClass(borderColor)
            : "border-white dark:border-gray-800"
        }`
      : "",
  className,
);

export const propTypes = {
  src: { type: "ImageMetadata", description: "Source image for the avatar" },
  alt: { type: "string", description: "Alt text for the avatar image" },
  initials: {
    type: "string",
    description: "Initials to display when no image is provided",
  },
  icon: {
    type: "string",
    description: "Icon to display when no image or initials are provided",
  },
  shape: {
    type: ["circular", "rounded", "square"],
    description: "Shape of the avatar",
    default: "circular",
  },
  size: {
    type: ["xs", "sm", "md", "lg", "xl", "2xl"],
    description: "Size of the avatar",
    default: "md",
  },
  bordered: {
    type: "boolean",
    description: "Whether to add a border to the avatar",
    default: false,
  },
  status: {
    type: ["online", "offline", "away", "busy", "dnd", "invisible"],
    description: "Status indicator for the avatar",
  },
  statusPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the status indicator",
    default: "bottom-right",
  },
  shadow: {
    type: ["sm", "md", "lg", "xl", "2xl", "none"],
    description: "Shadow size for the avatar",
    default: "none",
  },
  color: {
    type: Object.keys(colorPalette),
    description: "Color theme for the avatar",
    default: "gray",
  },
  colorIntensity: {
    type: "ColorIntensity",
    description: "Intensity of the color theme",
    default: "default",
  },
  variant: {
    type: ["filled", "outline"],
    description: "Visual variant of the avatar",
    default: "filled",
  },
  badge: { type: "number", description: "Number to display as a badge" },
  badgePosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the badge",
    default: "top-right",
  },
  logo: {
    type: "ImageMetadata",
    description: "Logo image to display on the avatar",
  },
  logoPosition: {
    type: ["bottom-right", "bottom-left", "top-right", "top-left"],
    description: "Position of the logo",
    default: "bottom-right",
  },
  badgeColor: {
    type: Object.keys(colorPalette),
    description: "Color of the badge",
    default: "red",
  },
  borderColor: {
    type: Object.keys(colorPalette),
    description: "Color of the avatar border when bordered is true",
  },
};

// Use 'badge' variant for badge colors
const badgeColorClasses = getBadgeClasses(badgeColor);
---

<div class={containerClasses}>
  <div class={contentClasses}>
    {
      src ? (
        <Image
          src={src}
          alt={alt || ""}
          width={400}
          height={400}
          class="w-full h-full object-cover"
        />
      ) : initials ? (
        <span class="font-medium">{initials}</span>
      ) : (
        <Icon name={icon || "UserIcon"} size={size} />
      )
    }
  </div>
  {
    logo && (
      <span
        class={`absolute ${getIndicatorPosition(logoPosition, size, false)} w-[40%] h-[40%] bg-white rounded-full overflow-hidden border-2 border-white shadow-sm`}
      >
        <Image
          src={logo}
          alt="Company logo"
          width={160}
          height={160}
          class="w-full h-full object-contain p-[1px]"
        />
      </span>
    )
  }
  {
    status && (
      <span
        class={`absolute ${getIndicatorSize(size, true)} ${getIndicatorPosition(statusPosition, size, true)} ${getStatusColor(status)} rounded-full border-2 border-white dark:border-gray-800`}
      />
    )
  }
  {
    badge !== undefined && (
      <span
        class={`absolute ${getIndicatorSize(size, false)} ${getIndicatorPosition(badgePosition, size, false)} ${badgeColorClasses} rounded-full flex items-center justify-center font-bold border-2 border-white dark:border-gray-800`}
      >
        {badge}
      </span>
    )
  }
</div>

Component Properties

Property Type Default Description
src ImageMetadata - Source image for the avatar
alt string - Alt text for the avatar image
initials string - Initials to display when no image is provided
icon string - Icon to display when no image or initials are provided
shape circular | rounded | square "circular" Shape of the avatar
size xs | sm | md | lg | xl | 2xl "md" Size of the avatar
bordered boolean false Whether to add a border to the avatar
status online | offline | away | busy | dnd | invisible - Status indicator for the avatar
statusPosition bottom-right | bottom-left | top-right | top-left "bottom-right" Position of the status indicator
shadow sm | md | lg | xl | 2xl | none "none" Shadow size for the avatar
color white | slate | gray | zinc | neutral | stone | red | orange | amber | yellow | lime | green | emerald | teal | cyan | sky | blue | indigo | violet | purple | fuchsia | pink | rose "gray" Color theme for the avatar
colorIntensity ColorIntensity "default" Intensity of the color theme
variant filled | outline "filled" Visual variant of the avatar
badge number - Number to display as a badge
badgePosition bottom-right | bottom-left | top-right | top-left "top-right" Position of the badge
logo ImageMetadata - Logo image to display on the avatar
logoPosition bottom-right | bottom-left | top-right | top-left "bottom-right" Position of the logo
badgeColor white | slate | gray | zinc | neutral | stone | red | orange | amber | yellow | lime | green | emerald | teal | cyan | sky | blue | indigo | violet | purple | fuchsia | pink | rose "red" Color of the badge
borderColor white | slate | gray | zinc | neutral | stone | red | orange | amber | yellow | lime | green | emerald | teal | cyan | sky | blue | indigo | violet | purple | fuchsia | pink | rose - Color of the avatar border when bordered is true