Avatar group
- Source
- After bencho.dev ↗
- Author
- Interaction after bencho.dev
- Licence
- Original code — interaction after bencho.dev
Loading…
Hover the stack to fan the avatars out and see who is who.
Code
"use client";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { Surface } from "../ui";
const PEOPLE = [
{ name: "Ana Ruiz", role: "Design", bg: "#f2c14e" },
{ name: "Ben Ito", role: "Engineering", bg: "#9db3f2" },
{ name: "Cleo Park", role: "Research", bg: "#e9a3c9" },
{ name: "Dev Shah", role: "Product", bg: "#8fd1b5" },
{ name: "Elle Moss", role: "Brand", bg: "#f39c7a" },
];
const MORE = 4;
const initials = (n: string) =>
n
.split(" ")
.map((p) => p[0])
.join("");
export default function AvatarGroup() {
const [open, setOpen] = useState(false);
const [hot, setHot] = useState<number | null>(null);
return (
<Surface>
<div className="flex flex-col items-center gap-[1.4em]">
<div
className="flex items-center"
onPointerEnter={() => setOpen(true)}
onPointerLeave={() => {
setOpen(false);
setHot(null);
}}
onClick={() => setOpen(true)}
>
{PEOPLE.map((p, i) => (
<motion.div
key={p.name}
className="relative"
animate={{ marginLeft: i === 0 ? "0em" : open ? "0.35em" : "-0.8em", y: hot === i ? -6 : 0 }}
transition={{ type: "spring", stiffness: 380, damping: 28 }}
style={{ zIndex: hot === i ? 20 : PEOPLE.length - i }}
onPointerEnter={() => setHot(i)}
onPointerDown={() => setHot(i)}
>
<motion.div
animate={{ scale: hot === i ? 1.12 : 1 }}
className="grid h-[3em] w-[3em] cursor-pointer place-items-center rounded-full border-[0.18em] border-[#f1f0ec] text-[0.95em] font-medium text-[#151515]"
style={{ background: p.bg }}
>
{initials(p.name)}
</motion.div>
<AnimatePresence>
{hot === i && (
<motion.div
initial={{ opacity: 0, y: 6, scale: 0.9, x: "-50%" }}
animate={{ opacity: 1, y: 0, scale: 1, x: "-50%" }}
exit={{ opacity: 0, y: 4, scale: 0.95, x: "-50%" }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
className="pointer-events-none absolute bottom-full left-1/2 mb-[0.6em] whitespace-nowrap rounded-[0.6em] bg-[#151515] px-[0.7em] py-[0.35em] text-center text-white"
>
<p className="text-[0.75em] font-medium">{p.name}</p>
<p className="text-[0.62em] text-white/60">{p.role}</p>
</motion.div>
)}
</AnimatePresence>
</motion.div>
))}
<motion.div
animate={{ marginLeft: open ? "0.35em" : "-0.8em" }}
transition={{ type: "spring", stiffness: 380, damping: 28 }}
className="relative z-0 grid h-[3em] w-[3em] place-items-center rounded-full border-[0.18em] border-[#f1f0ec] bg-white text-[0.8em] font-medium text-[#8a8984]"
>
+{MORE}
</motion.div>
</div>
<p className="font-mono text-[0.7em] text-[#8a8984]">{PEOPLE.length + MORE} people on this file</p>
</div>
</Surface>
);
}
Prompt
Build "Avatar group", a other animation. What it does: Hover the stack to fan the avatars out and see who is who. Write it as a single self-contained React component in TypeScript that fills its parent (w-full h-full), styled with Tailwind CSS, using motion. Technique: an HTML canvas, the Motion library, pointer tracking. It sits on a light background, in a 16:10 frame. Respect prefers-reduced-motion with a still state, pause work when the element is off screen, and keep it accessible. Original: https://bencho.dev (Interaction after bencho.dev, Original code — interaction after bencho.dev).