Avatar Stack

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

Add to your project

Pick the way you like to build.

Let your AI assistant add it

Copy this prompt and paste it into the AI tool building your site.

# Integrate this UI component

Add this component to my project.

1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.

## Component source

```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>
  );
}

```

Works with your AI tools

ClaudeClaude*
CodexCodex*
LovableLovable
Base44Base44
Figma MakeFigma Make
ReplitReplit*
CursorCursor*
v0v0

* Compatible with most projects that use the default React/Next.js framework.

Built With

ReactTailwind CSSFramer Motion

Props

PropTypeDescription
avatarsAvatar[]Portrait entries — image src, alt, optional name
maxVisiblenumberHow many faces show before the +N counter
labelstringOptional caption under the stack
size'sm' | 'md' | 'lg'Avatar diameter
showNamesbooleanReveal names on hover
classNamestringStyles for the outer shell

More Trust Components

4.9· 1.3k reviews
Rated 4.9 out of 5 from 1280 reviews on G2

Editorial rating with sequentially drawn stars, review count, and a soft luminous edge.