litenBeta

Avatar Group

Overlapping avatars that spread apart on hover, each lifting with a name tooltip.

Maya OkaforMaya Okafor
Tomás RiveraTomás Rivera
Alma KideAlma Kide
Jonas WeberJonas Weber
Priya NairPriya Nair
+3

Trusted 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.

Terminal
bun add motion

Copy the component into components/ui/avatar-group.tsx.

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(              'pointer-events-none absolute -top-9 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-lg border border-black/[0.08] px-2 py-1',              'text-[12px] font-medium tracking-[-0.01em]',              'bg-white text-neutral-900 dark:border-white/[0.08] dark:bg-[#232320] dark:text-white',              'shadow-[0_2px_6px_-2px_rgba(27,27,25,0.16)] dark:shadow-[0_4px_10px_-4px_rgba(0,0,0,0.5)]',              'opacity-0 transition-opacity duration-150 group-hover/av:opacity-100',            )}          >            {a.name}          </span>          <span            className="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(            'grid place-items-center rounded-full font-semibold ring-2 ring-white dark:ring-[#0a0a0b]',            'bg-white text-neutral-600 dark:bg-[#232320] 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

Example.tsx
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.

Maya OkaforMaya Okafor
Tomás RiveraTomás Rivera
Alma KideAlma Kide
Jonas WeberJonas Weber
Maya OkaforMaya Okafor
Tomás RiveraTomás Rivera
Alma KideAlma Kide
Jonas WeberJonas Weber
+4

Props

PropTypeDefaultDescription
avatarsAvatar[]-List of { src?, name } entries to render.
maxnumber5How many to show before collapsing into a "+N" chip.
sizenumber40Avatar diameter in px.
classNamestring-Forwarded to the root.
On this page0%