shadimage
uniquel
componentavatar-stacksource: ok · hit

Avatar Stack

Layered portraits with grayscale-to-color hover, spring lift, and a glass +N counter.

npx shadcn add "https://uniquel.io/r/avatar-stack.json"
free

Where it goes

stats

Visual style

glass-blur

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · medium
animationframer-motion

Files1 · source cached 10/14/2026, 1:21:41 PM

components/trust/AvatarStack.tsx
'use client';

import { motion } from 'framer-motion';

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// avatars       Portrait entries — image src, alt, optional name
// maxVisible    How many faces show before the +N counter
// label         Optional caption under the stack (e.g. "Trusted by 2.4k teams")
// size          Avatar diameter: sm | md | lg
// showNames     Reveal names on hover
// className     Styles for the outer shell
//
type Avatar = {
  src: string;
  alt?: string;
  name?: string;
};

type AvatarStackProps = {
  avatars?: Avatar[];
  maxVisible?: number;
  label?: string;
  size?: 'sm' | 'md' | 'lg';
  showNames?: boolean;
  className?: string;
};

const DEFAULT_AVATARS: Avatar[] = [
  {
    src: 'https://assets.uniquel.io/components/stock/photo-1494790108377-be9c29b29330.jpg',
    alt: 'Maya Chen',
    name: 'Maya Chen',
  },
  {
    src: 'https://assets.uniquel.io/components/stock/photo-1507003211169-0a1dd7228f2d.jpg',
    alt: 'James Okonkwo',
    name: 'James Okonkwo',
  },
  {
    src: 'https://assets.uniquel.io/components/stock/photo-1438761681033-6461ffad8d80.jpg',
    alt: 'Sofia Reyes',
    name: 'Sofia Reyes',
  },
  {
    src: 'https://assets.uniquel.io/components/stock/photo-1472099645785-5658abf4ff4e.jpg',
    alt: 'Noah Berg',
    name: 'Noah Berg',
  },
  {
    src: 'https://assets.uniquel.io/components/stock/photo-1544005313-94ddf0286df2.jpg',
    alt: 'Aria Patel',
    name: 'Aria Patel',
  },
  {
    src: 'https://assets.uniquel.io/components/stock/photo-1500648767791-00dcc994a43e.jpg',
    alt: 'Liam Frost',
    name: 'Liam Frost',
  },
];

const SIZE_MAP = {
  sm: { px: 28, ring: 2, overlap: -10, text: 'text-[10px]' },
  md: { px: 36, ring: 2, overlap: -12, text: 'text-xs' },
  lg: { px: 44, ring: 3, overlap: -14, text: 'text-sm' },
};

export default function AvatarStack({
  avatars = DEFAULT_AVATARS,
  maxVisible = 4,
  label = 'Joined by 2.4k builders',
  size = 'md',
  showNames = true,
  className = '',
}: AvatarStackProps) {
  const deck = avatars.length ? avatars : DEFAULT_AVATARS;
  const visible = deck.slice(0, maxVisible);
  const overflow = Math.max(0, deck.length - maxVisible);
  const s = SIZE_MAP[size];

  return (
    <div
      className={['inline-flex flex-col items-start gap-2.5', className]
        .filter(Boolean)
        .join(' ')}
    >
      <div className='flex items-center'>
        {visible.map((avatar, index) => (
          <motion.div
            key={`${avatar.src}-${index}`}
            className='group relative'
            style={{
              marginLeft: index === 0 ? 0 : s.overlap,
              zIndex: index + 1,
            }}
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{
              duration: 0.45,
              delay: index * 0.06,
              ease: [0.22, 1, 0.36, 1],
            }}
            whileHover={{
              y: -4,
              zIndex: 20,
              transition: { type: 'spring', stiffness: 400, damping: 22 },
            }}
          >
            <div
              className='overflow-hidden rounded-full bg-white shadow-[0_1px_2px_rgba(0,0,0,0.08),0_4px_12px_rgba(0,0,0,0.06)]'
              style={{
                width: s.px,
                height: s.px,
                outline: `${s.ring}px solid white`,
              }}
            >
              <img
                src={avatar.src}
                alt={avatar.alt ?? avatar.name ?? ''}
                className='h-full w-full object-cover grayscale transition-[filter,transform] duration-500 group-hover:scale-105 group-hover:grayscale-0'
              />
            </div>
            {showNames && avatar.name ? (
              <span className='pointer-events-none absolute -bottom-7 left-1/2 z-30 -translate-x-1/2 whitespace-nowrap rounded-full bg-gray-900/90 px-2 py-0.5 text-[10px] font-medium tracking-wide text-white opacity-0 shadow-lg backdrop-blur-xs transition-opacity duration-200 group-hover:opacity-100'>
                {avatar.name}
              </span>
            ) : null}
          </motion.div>
        ))}

        {overflow > 0 ? (
          <motion.div
            className='relative flex items-center justify-center rounded-full border border-white/60 bg-white/70 font-medium tracking-wide text-gray-700 shadow-[0_1px_2px_rgba(0,0,0,0.06)] backdrop-blur-md'
            style={{
              width: s.px,
              height: s.px,
              marginLeft: s.overlap,
              zIndex: visible.length + 1,
              fontSize: size === 'sm' ? 10 : size === 'md' ? 11 : 12,
            }}
            initial={{ opacity: 0, scale: 0.85 }}
            animate={{ opacity: 1, scale: 1 }}
            transition={{
              duration: 0.4,
              delay: visible.length * 0.06,
              ease: [0.22, 1, 0.36, 1],
            }}
            whileHover={{
              y: -3,
              transition: { type: 'spring', stiffness: 400, damping: 22 },
            }}
          >
            +{overflow}
          </motion.div>
        ) : null}
      </div>

      {label ? (
        <motion.p
          className={['font-medium tracking-wide text-gray-500', s.text].join(
            ' '
          )}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          transition={{ duration: 0.5, delay: 0.35 }}
        >
          {label}
        </motion.p>
      ) : null}
    </div>
  );
}

More from uniquel

Browse all components in this library →