shadimage
tween-ui
uiavatar-revealsource: ok · hit

Avatar Reveal

Three stacked avatars that scale and unblur in from the left on an elastic ease, then a caption slides in from the right.

npx shadcn add "https://tween-ui.vercel.app/r/avatar-reveal.json"
free

Visual style

glass-blur3d

Interaction

scroll-reveal

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

Files1 · source cached 10/14/2026, 2:44:04 PM

components/tweenui/avatar-reveal.tsx
'use client';

import { useRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { cn } from '@/lib/utils';

gsap.registerPlugin(useGSAP);

const prefersReducedMotion = () =>
  typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;

export interface AvatarRevealItem {
  src: string;
  alt: string;
}

const DEFAULT_AVATARS: AvatarRevealItem[] = [
  {
    src: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=88&h=88&fit=crop&crop=faces',
    alt: 'Team member 1',
  },
  {
    src: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=88&h=88&fit=crop&crop=faces',
    alt: 'Team member 2',
  },
  {
    src: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=88&h=88&fit=crop&crop=faces',
    alt: 'Team member 3',
  },
];

interface AvatarRevealProps extends Omit<ComponentPropsWithoutRef<'div'>, 'children'> {
  avatars?: AvatarRevealItem[];
  children?: ReactNode;
}

export default function AvatarReveal({
  avatars = DEFAULT_AVATARS,
  children = '2,000+ teams shipping faster this week.',
  className,
  ...props
}: AvatarRevealProps) {
  const rootRef = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      const root = rootRef.current;
      if (!root) return;

      const items = gsap.utils.toArray<HTMLElement>('[data-ns-avatar]', root);
      const caption = root.querySelector<HTMLElement>('[data-ns-animate]');

      if (prefersReducedMotion()) {
        gsap.set([items, caption], { opacity: 1, x: 0, scale: 1, filter: 'none' });
        return;
      }

      gsap.fromTo(
        items,
        { opacity: 0, scale: 0, x: -40, filter: 'blur(5px)' },
        {
          opacity: 1,
          scale: 1,
          x: 0,
          filter: 'blur(0px)',
          duration: 1.5,
          stagger: 0.1,
          delay: 0.1,
          ease: 'elastic.out(1, 0.7)',
        }
      );

      if (caption) {
        gsap.fromTo(
          caption,
          { opacity: 0, x: 50 },
          { opacity: 1, x: 0, duration: 0.8, delay: 0.2, ease: 'power2.out' }
        );
      }
    },
    { scope: rootRef }
  );

  return (
    <div
      {...props}
      ref={rootRef}
      data-avatar-reveal
      className={cn('mx-auto flex max-w-[254px] items-center justify-center gap-x-3', className)}
    >
      <div className="flex items-center justify-center -space-x-3.5">
        {avatars.map((avatar) => (
          <figure
            key={avatar.src}
            data-ns-avatar
            className="size-11 overflow-hidden rounded-full opacity-0 outline outline-2 outline-white motion-reduce:opacity-100"
          >
            <img
              src={avatar.src}
              alt={avatar.alt}
              className="size-full rounded-full object-cover"
            />
          </figure>
        ))}
      </div>
      {children ? (
        <p
          data-ns-animate
          className="mx-auto max-w-[142px] text-left text-sm text-[#045f64]/80 opacity-0 motion-reduce:opacity-100 dark:text-[#9fd4d6]/80"
        >
          {children}
        </p>
      ) : null}
    </div>
  );
}

More from tween-ui

Browse all components in this library →