# Avatar Group Tooltip Names — React Avatar Component

> An avatar row sharing one tooltip that glides from face to face on a spring while the hovered avatar lifts and its neighbours step aside.

- Category: [Avatars](https://pixelpapa.com/components/avatars.md)
- Tags: avatar, group, tooltip, hover, spring
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/avatars/avatar-group-tooltip-names

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/avatar-group-tooltip-names.json"
```

The file is written to `components/pixelpapa/avatar-group-tooltip-names.tsx`. To install manually, run `npm install motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { AnimatePresence, motion } from "motion/react";
import { useState, type FocusEvent } from "react";
import { cn } from "@/lib/utils";

type Member = { name: string; role: string; initials: string; hue: number };

type AvatarGroupProps = {
  members: Member[];
  size?: number;
  /** How far neighbours overlap, in px. */
  overlap?: number;
  /** Show a tooltip before anyone hovers. */
  defaultActive?: number | null;
  className?: string;
};

const spring = { type: "spring", stiffness: 400, damping: 28 } as const;

export function AvatarGroup({ members, size = 56, overlap = 14, defaultActive = null, className }: AvatarGroupProps) {
  const [active, setActive] = useState<number | null>(defaultActive);
  const step = size - overlap;
  const centre = active === null ? 0 : active * step + size / 2;

  function onBlur(e: FocusEvent<HTMLDivElement>) {
    if (!e.currentTarget.contains(e.relatedTarget)) setActive(null);
  }

  return (
    <div className={cn("relative inline-flex", className)} onPointerLeave={() => setActive(null)} onBlur={onBlur}>
      <AnimatePresence>
        {active !== null && (
          <motion.div
            role="tooltip"
            className="pointer-events-none absolute bottom-full left-0 z-30 mb-5"
            initial={{ opacity: 0, y: 8, x: centre }}
            animate={{ opacity: 1, y: 0, x: centre }}
            exit={{ opacity: 0, y: 6, transition: { duration: 0.12 } }}
            transition={spring}
          >
            <div className="relative -translate-x-1/2 rounded-xl bg-foreground px-3.5 py-2 text-center text-background shadow-xl">
              <AnimatePresence mode="popLayout" initial={false}>
                <motion.div key={active} initial={{ opacity: 0, filter: "blur(4px)" }} animate={{ opacity: 1, filter: "blur(0px)" }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }}>
                  <p className="text-sm font-medium whitespace-nowrap">{members[active].name}</p>
                  <p className="text-[11px] whitespace-nowrap opacity-65">{members[active].role}</p>
                </motion.div>
              </AnimatePresence>
              <span className="absolute top-full left-1/2 -mt-1.5 size-3 -translate-x-1/2 rotate-45 rounded-[3px] bg-foreground" />
            </div>
          </motion.div>
        )}
      </AnimatePresence>
      {members.map((m, i) => (
        <motion.button
          key={m.name}
          type="button"
          aria-label={`${m.name}, ${m.role}`}
          onPointerEnter={() => setActive(i)}
          onFocus={() => setActive(i)}
          animate={{
            y: active === i ? -10 : 0,
            scale: active === i ? 1.14 : 1,
            x: active === null || active === i ? 0 : i < active ? -5 : 5,
          }}
          transition={spring}
          className="relative flex shrink-0 items-center justify-center rounded-full font-semibold text-white ring-[3px] ring-background outline-none focus-visible:ring-ring"
          style={{
            width: size,
            height: size,
            fontSize: size * 0.34,
            marginLeft: i ? -overlap : 0,
            zIndex: active === i ? 20 : members.length - i,
            background: `linear-gradient(135deg, oklch(0.78 0.14 ${m.hue}), oklch(0.54 0.2 ${m.hue + 40}))`,
          }}
        >
          {m.initials}
        </motion.button>
      ))}
    </div>
  );
}

const editors: Member[] = [
  { name: "Saanvi Rao", role: "Product lead", initials: "SR", hue: 262 },
  { name: "Mateo Vargas", role: "iOS engineer", initials: "MV", hue: 30 },
  { name: "Ingrid Holm", role: "Brand designer", initials: "IH", hue: 330 },
  { name: "Kofi Mensah", role: "Data scientist", initials: "KM", hue: 155 },
  { name: "Lena Fischer", role: "Researcher", initials: "LF", hue: 205 },
  { name: "Yusuf Kaya", role: "Backend engineer", initials: "YK", hue: 80 },
];

export default function AvatarGroupTooltipNamesDemo() {
  return (
    <div className="flex flex-col items-center gap-5 pt-16">
      <AvatarGroup members={editors} defaultActive={2} />
      <p className="flex items-center gap-2 text-sm text-muted-foreground">
        <span className="size-2 rounded-full bg-emerald-500" /> 6 people editing <span className="font-medium text-foreground">Q1 roadmap</span>
      </p>
    </div>
  );
}
```

## AI prompt

```text
Add the "Avatar Group Tooltip Names" component from PixelPapa UI to this project.

What it is: An avatar row sharing one tooltip that glides from face to face on a spring while the hovered avatar lifts and its neighbours step aside.
Category: Avatars. Depends on: motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/avatar-group-tooltip-names.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/avatar-group-tooltip-names.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/avatar-group-tooltip-names" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More avatars

- [Avatar Hover Card](https://pixelpapa.com/components/avatars/avatar-hover-card.md) (Pro): An avatar that opens a profile popover on hover or focus, scaling out of the trigger on a spring with forgiving open and close delays.
- [Avatar Stack Expand](https://pixelpapa.com/components/avatars/avatar-stack-expand.md): An overlapping avatar stack that springs apart on hover to show first names, with a +N chip that deals out the rest.
- [Avatar Status Ring](https://pixelpapa.com/components/avatars/avatar-status-ring.md): An avatar wrapped in a presence ring: online radiates soft pulses, away dims to amber, busy holds a solid red, and the badge pops when status changes.
- [Avatar Story Ring](https://pixelpapa.com/components/avatars/avatar-story-ring.md) (Pro): Story avatars with a gradient ring that breaks into spinning dashes while the story loads, then settles into a quiet seen state.
- [Avatar Upload Dropzone](https://pixelpapa.com/components/avatars/avatar-upload-dropzone.md) (Pro): A circular avatar dropzone: its dashed ring spins and swells when a file hovers over it, then the photo drops in over the initials fallback.
