Baktiyar Ablaikhan

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