Avatar Group with Tooltip
A visually interactive avatar group with hover effects and tooltips, allowing users to easily view and highlight avatars with smooth animations.
Preview
JD
SS
AW
EJ
Installation
Install dependencies
# Install Shadcn Avatar component
npx shadcn@latest add avatar
# Install Shadcn Tooltip component
npx shadcn@latest add tooltip
Create the component file
mkdir -p components/shsfui/avatar && touch components/shsfui/avatar/avatar-group-with-tooltip.tsx
Add the component code
Open the newly created file and paste the following code:
"use client";
import * as React from "react";
import {
Avatar,
AvatarImage,
AvatarFallback,
} from "@/components/ui/avatar";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/utils/cn";
type AvatarWithTooltipType = {
src: string;
name: string;
status?: keyof typeof statusColors;
};
type AvatarGroupWithTooltipsProps = {
avatars?: AvatarWithTooltipType[];
maxDisplay?: number;
showStatus?: boolean;
delayDuration?: number;
className?: string;
};
export const DEFAULT_AVATARS: AvatarWithTooltipType[] = [
{
src: "https://randomuser.me/api/portraits/men/32.jpg",
name: "John Doe",
status: "online",
},
{
src: "https://randomuser.me/api/portraits/women/44.jpg",
name: "Sarah Smith",
status: "busy",
},
{
src: "https://randomuser.me/api/portraits/men/91.jpg",
name: "Alex Wong",
status: "away",
},
{
src: "https://randomuser.me/api/portraits/women/17.jpg",
name: "Emma Johnson",
status: "offline",
},
];
const statusColors = {
online: "bg-green-500",
busy: "bg-red-500",
away: "bg-amber-500",
offline: "bg-gray-400",
};
const getInitials = (name: string): string => {
return name
.split(" ")
.map((part) => part[0])
.join("")
.toUpperCase();
};
const AvatarGroupWithTooltips = React.forwardRef<
HTMLDivElement,
AvatarGroupWithTooltipsProps
>((props, ref) => {
const {
avatars = DEFAULT_AVATARS,
maxDisplay = avatars.length,
showStatus = false,
delayDuration = 300,
className,
} = props;
const displayAvatars = React.useMemo(
() => avatars.slice(0, maxDisplay),
[avatars, maxDisplay]
);
const remainingCount = avatars.length - maxDisplay;
return (
<TooltipProvider delayDuration={delayDuration}>
<div
ref={ref}
className={cn(
"bg-background flex items-center justify-center rounded-full border p-1",
className
)}
>
<div className="flex items-center relative">
{displayAvatars.map((avatar, index) => (
<Tooltip key={index}>
<TooltipTrigger asChild>
<div
className={cn("relative hover:z-10", index > 0 && "-ml-2")}
>
<Avatar className="transition-all duration-300 hover:scale-105 hover:-translate-y-1 hover:shadow-lg border-2 border-background">
<AvatarImage src={avatar.src} alt={avatar.name} />
<AvatarFallback>{getInitials(avatar.name)}</AvatarFallback>
</Avatar>
{showStatus && avatar.status && (
<span
className={cn(
"absolute bottom-0 right-0 block h-2 w-2 rounded-full ring-2 ring-background",
statusColors[avatar.status]
)}
/>
)}
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="font-medium">
{avatar.name}
{showStatus && avatar.status && (
<span className="block text-xs capitalize text-muted-foreground">
{avatar.status}
</span>
)}
</TooltipContent>
</Tooltip>
))}
{remainingCount > 0 && (
<Tooltip>
<TooltipTrigger asChild>
<div className={cn("relative hover:z-10", "-ml-2")}>
<Avatar className="transition-all duration-300 hover:scale-105 hover:-translate-y-1 hover:shadow-lg border-2 border-background bg-muted">
<AvatarFallback>+{remainingCount}</AvatarFallback>
</Avatar>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="font-medium">
{remainingCount} more {remainingCount === 1 ? "user" : "users"}
</TooltipContent>
</Tooltip>
)}
</div>
</div>
</TooltipProvider>
);
});
AvatarGroupWithTooltips.displayName = "AvatarGroupWithTooltips";
export AvatarGroupWithTooltips;