Avatar Group
Overlapping avatars that spread apart on hover, each lifting with a name tooltip.
Ada LovelaceAL
Alan TuringAT
Grace HopperGH
Linus TorvaldsLT
Margaret HamiltonMH
+3Trusted by 12,000+ builders
Avatars stack with a slight overlap by default and springs apart just enough to reveal their edges on hover, while the hovered avatar itself lifts above its neighbors and shows a name tooltip. Images fall back to colored initials chips, hashed deterministically from the name so the palette is stable across renders. Built with Motion.
Installation
Complete the shared Setup first, then add Motion.
bun add motionCopy the component into components/ui/avatar-group.tsx.
'use client';import * as React from 'react';import { motion } from 'motion/react';import { cn } from '@/lib/cn';const PALETTE = ['#f0883e', '#3ecf8e', '#22d3ee', '#b06bff', '#f0463a'];export type Avatar = { src?: string; name: string;};export type AvatarGroupProps = { avatars: Avatar[]; max?: number; size?: number; className?: string;};function initials(name: string) { return name .split(' ') .map((w) => w[0]) .slice(0, 2) .join('') .toUpperCase();}export function AvatarGroup({ avatars, max = 5, size = 40, className,}: AvatarGroupProps) { const [open, setOpen] = React.useState(false); const shown = avatars.slice(0, max); const extra = avatars.length - shown.length; const overlap = size * 0.34; const spread = size * 0.18; return ( <motion.div onHoverStart={() => setOpen(true)} onHoverEnd={() => setOpen(false)} className={cn('flex items-center', className)} > {shown.map((a, i) => ( <motion.div key={a.name + i} className="group/av relative" style={{ zIndex: shown.length - i }} initial={false} animate={{ marginLeft: i === 0 ? 0 : open ? -overlap + spread : -overlap }} transition={{ type: 'spring', stiffness: 320, damping: 26 }} whileHover={{ y: -4, zIndex: 50, transition: { duration: 0.18 } }} > <span className={cn( 'bloom-edge pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-lg px-2 py-1', 'text-[12px] font-medium tracking-[-0.01em]', 'bg-gradient-to-b from-white to-[#f4f4f5] text-neutral-900', 'dark:from-[#272727] dark:to-[#191919] dark:text-white', 'shadow-[0_4px_12px_-4px_rgba(0,0,0,0.2)] dark:shadow-[0_6px_16px_-8px_rgba(0,0,0,0.6)]', 'opacity-0 transition-opacity duration-150 group-hover/av:opacity-100', )} > {a.name} </span> <span className="bloom-edge block overflow-hidden rounded-full ring-2 ring-white dark:ring-[#0a0a0b]" style={{ width: size, height: size }} > {a.src ? ( <img src={a.src} alt={a.name} className="size-full object-cover" draggable={false} /> ) : ( <span className="flex size-full items-center justify-center font-semibold" style={{ background: PALETTE[hashName(a.name) % PALETTE.length], color: '#141612', fontSize: size * 0.36, }} > {initials(a.name)} </span> )} </span> </motion.div> ))} {extra > 0 && ( <motion.span initial={false} animate={{ marginLeft: open ? -overlap + spread : -overlap }} transition={{ type: 'spring', stiffness: 320, damping: 26 }} className={cn( 'bloom-edge grid place-items-center rounded-full font-semibold ring-2 ring-white dark:ring-[#0a0a0b]', 'bg-gradient-to-b from-white to-[#f4f4f5] text-neutral-600', 'dark:from-[#242424] dark:to-[#1a1a1a] dark:text-neutral-300', )} style={{ width: size, height: size, fontSize: size * 0.32, zIndex: 0 }} > +{extra} </motion.span> )} </motion.div> );}function hashName(name: string) { let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) | 0; return Math.abs(h);}Usage
import { AvatarGroup } from '@/components/ui/avatar-group';
const PEOPLE = [
{ name: 'Ada Lovelace' },
{ name: 'Alan Turing' },
{ name: 'Grace Hopper' },
];
export default function Example() {
return <AvatarGroup avatars={PEOPLE} max={5} />;
}Examples
Two sizes side by side.
Ada LovelaceAL
Alan TuringAT
Grace HopperGH
Linus TorvaldsLT
Ada LovelaceAL
Alan TuringAT
Grace HopperGH
Linus TorvaldsLT
+4Props
| Prop | Type | Default | Description |
|---|---|---|---|
avatars | Avatar[] | - | List of { src?, name } entries to render. |
max | number | 5 | How many to show before collapsing into a "+N" chip. |
size | number | 40 | Avatar diameter in px. |
className | string | - | Forwarded to the root. |