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<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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>
<div
class="flex -space-x-4 grid-cols-4 grid-rows-1 items-center gap-12 p-4 [&>*]:z-0 [&>*: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 [&>*]:z-0 [&>*: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 |