Avatar Group
Versatile avatar group component with motion, CSS, and stack variants. Features tooltips, hover effects, and flexible positioning for React applications with Next.js integration and TypeScript support.
Powered by
'use client';import { Avatar, AvatarFallback, AvatarImage,} from '@/components/ui/avatar';import { AvatarGroup, AvatarGroupTooltip,} from '@/components/ui/shadcn-io/avatar-group'; const AVATARS = [ { src: 'https://pbs.twimg.com/profile_images/1909615404789506048/MTqvRsjo_400x400.jpg', fallback: 'SK', tooltip: 'Skyleen', }, { src: 'https://pbs.twimg.com/profile_images/1593304942210478080/TUYae5z7_400x400.jpg', fallback: 'CN', tooltip: 'Shadcn', }, { src: 'https://pbs.twimg.com/profile_images/1677042510839857154/Kq4tpySA_400x400.jpg', fallback: 'AW', tooltip: 'Adam Wathan', }, { src: 'https://pbs.twimg.com/profile_images/1783856060249595904/8TfcCN0r_400x400.jpg', fallback: 'GR', tooltip: 'Guillermo Rauch', }, { src: 'https://pbs.twimg.com/profile_images/1534700564810018816/anAuSfkp_400x400.jpg', fallback: 'JH', tooltip: 'Jhey', },]; export const AvatarGroupDemo = () => { return ( <AvatarGroup variant="motion" className="h-12 -space-x-3"> {AVATARS.map((avatar, index) => ( <Avatar key={index} className="size-12 border-3 border-background"> <AvatarImage src={avatar.src} /> <AvatarFallback>{avatar.fallback}</AvatarFallback> <AvatarGroupTooltip> <p>{avatar.tooltip}</p> </AvatarGroupTooltip> </Avatar> ))} </AvatarGroup> );};export default AvatarGroupDemo;
'use client';import * as React from 'react';import { motion, type Transition } from 'motion/react';import { Children, type ReactNode } from 'react';import { cn } from '@/lib/utils';import { TooltipContent, TooltipProvider, TooltipTrigger,} from '@/components/ui/tooltip';import * as TooltipPrimitive from '@radix-ui/react-tooltip';// Define types based on componentstype TooltipContentProps = React.ComponentProps<typeof TooltipContent>;// Avatar Container for motion-based interactionstype AvatarMotionProps = { children: React.ReactNode; zIndex: number; translate: string | number; transition: Transition; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarMotionContainer({ children, zIndex, translate, transition, tooltipContent, tooltipProps,}: AvatarMotionProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <motion.div data-slot="avatar-container" className="relative" style={{ zIndex }} whileHover={{ y: translate, }} transition={transition} > {children} </motion.div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for CSS-based interactionstype AvatarCSSProps = { children: React.ReactNode; zIndex: number; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarCSSContainer({ children, zIndex, tooltipContent, tooltipProps,}: AvatarCSSProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <div data-slot="avatar-container" className="relative transition-transform duration-300 ease-out hover:-translate-y-2" style={{ zIndex }} > {children} </div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for stack variant with masktype AvatarStackItemProps = { children: React.ReactNode; index: number; size: number; className?: string;};function AvatarStackItem({ children, index, size, className }: AvatarStackItemProps) { return ( <div className={cn( 'size-full shrink-0 overflow-hidden rounded-full', '[&_[data-slot="avatar"]]:size-full', className )} style={{ width: size, height: size, maskImage: index ? `radial-gradient(circle ${size / 2}px at -${size / 4 + size / 10}px 50%, transparent 99%, white 100%)` : '', }} > {children} </div> );}type AvatarGroupTooltipProps = TooltipContentProps;function AvatarGroupTooltip(props: AvatarGroupTooltipProps) { return <TooltipContent {...props} />;}type AvatarGroupVariant = 'motion' | 'css' | 'stack';type AvatarGroupProps = Omit<React.ComponentProps<'div'>, 'translate'> & { children: React.ReactElement[]; variant?: AvatarGroupVariant; transition?: Transition; invertOverlap?: boolean; translate?: string | number; tooltipProps?: Partial<TooltipContentProps>; // Stack-specific props animate?: boolean; size?: number;};function AvatarGroup({ ref, children, className, variant = 'motion', transition = { type: 'spring', stiffness: 300, damping: 17 }, invertOverlap = false, translate = '-30%', tooltipProps = { side: 'top', sideOffset: 24 }, animate = false, size = 40, ...props}: AvatarGroupProps) { // Stack variant if (variant === 'stack') { return ( <div ref={ref} className={cn( '-space-x-1 flex items-center', animate && 'hover:space-x-0 [&>*]:transition-all', className )} {...props} > {Children.map(children, (child, index) => { if (!child) { return null; } return ( <AvatarStackItem key={index} index={index} size={size} className={className} > {child} </AvatarStackItem> ); })} </div> ); } // Motion and CSS variants with tooltips return ( <TooltipProvider delayDuration={0}> <div ref={ref} data-slot="avatar-group" className={cn( 'flex items-center', variant === 'css' && '-space-x-3', variant === 'motion' && 'flex-row -space-x-2 h-8', className )} {...props} > {children?.map((child, index) => { const zIndex = invertOverlap ? React.Children.count(children) - index : index; if (variant === 'motion') { return ( <AvatarMotionContainer key={index} zIndex={zIndex} translate={translate} transition={transition} tooltipProps={tooltipProps} > {child} </AvatarMotionContainer> ); } return ( <AvatarCSSContainer key={index} zIndex={zIndex} tooltipProps={tooltipProps} > {child} </AvatarCSSContainer> ); })} </div> </TooltipProvider> );}export { AvatarGroup, AvatarGroupTooltip, type AvatarGroupProps, type AvatarGroupTooltipProps, type AvatarGroupVariant,};
Installation
npx shadcn@latest add https://www.shadcn.io/registry/avatar-group.json
npx shadcn@latest add https://www.shadcn.io/registry/avatar-group.json
pnpm dlx shadcn@latest add https://www.shadcn.io/registry/avatar-group.json
bunx shadcn@latest add https://www.shadcn.io/registry/avatar-group.json
Features
- Multiple variants including motion-based, CSS-only, and stack layouts
- Spring animations with hover effects using customizable motion transitions
- Tooltip integration with shadcn/ui tooltip components and configurable positioning
- Z-index management with automatic layering and optional overlap inversion
- Flexible positioning with customizable translate values and spring physics
- TypeScript support with complete interface definitions and variant types
- shadcn/ui integration using design system avatar components and spacing utilities
- Responsive design that works across device sizes with touch-friendly interactions
Examples
CSS Variant
'use client';import { Avatar, AvatarFallback, AvatarImage,} from '@/components/ui/avatar';import { AvatarGroup, AvatarGroupTooltip,} from '@/components/ui/shadcn-io/avatar-group'; const AVATARS = [ { src: 'https://pbs.twimg.com/profile_images/1909615404789506048/MTqvRsjo_400x400.jpg', fallback: 'SK', tooltip: 'Skyleen', }, { src: 'https://pbs.twimg.com/profile_images/1593304942210478080/TUYae5z7_400x400.jpg', fallback: 'CN', tooltip: 'Shadcn', }, { src: 'https://pbs.twimg.com/profile_images/1677042510839857154/Kq4tpySA_400x400.jpg', fallback: 'AW', tooltip: 'Adam Wathan', }, { src: 'https://pbs.twimg.com/profile_images/1783856060249595904/8TfcCN0r_400x400.jpg', fallback: 'GR', tooltip: 'Guillermo Rauch', }, { src: 'https://pbs.twimg.com/profile_images/1534700564810018816/anAuSfkp_400x400.jpg', fallback: 'JH', tooltip: 'Jhey', },]; export const AvatarGroupCSSDemo = () => { return ( <div className="bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 p-0.5 rounded-full"> <div className="bg-gradient-to-r from-indigo-100 via-purple-100 to-pink-100 dark:from-indigo-950 dark:via-purple-950 dark:to-pink-950 p-1.5 rounded-full"> <AvatarGroup variant="css"> {AVATARS.map((avatar, index) => ( <Avatar key={index}> <AvatarImage src={avatar.src} /> <AvatarFallback>{avatar.fallback}</AvatarFallback> <AvatarGroupTooltip> <p>{avatar.tooltip}</p> </AvatarGroupTooltip> </Avatar> ))} </AvatarGroup> </div> </div> );};export default AvatarGroupCSSDemo;
'use client';import * as React from 'react';import { motion, type Transition } from 'motion/react';import { Children, type ReactNode } from 'react';import { cn } from '@/lib/utils';import { TooltipContent, TooltipProvider, TooltipTrigger,} from '@/components/ui/tooltip';import * as TooltipPrimitive from '@radix-ui/react-tooltip';// Define types based on componentstype TooltipContentProps = React.ComponentProps<typeof TooltipContent>;// Avatar Container for motion-based interactionstype AvatarMotionProps = { children: React.ReactNode; zIndex: number; translate: string | number; transition: Transition; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarMotionContainer({ children, zIndex, translate, transition, tooltipContent, tooltipProps,}: AvatarMotionProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <motion.div data-slot="avatar-container" className="relative" style={{ zIndex }} whileHover={{ y: translate, }} transition={transition} > {children} </motion.div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for CSS-based interactionstype AvatarCSSProps = { children: React.ReactNode; zIndex: number; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarCSSContainer({ children, zIndex, tooltipContent, tooltipProps,}: AvatarCSSProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <div data-slot="avatar-container" className="relative transition-transform duration-300 ease-out hover:-translate-y-2" style={{ zIndex }} > {children} </div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for stack variant with masktype AvatarStackItemProps = { children: React.ReactNode; index: number; size: number; className?: string;};function AvatarStackItem({ children, index, size, className }: AvatarStackItemProps) { return ( <div className={cn( 'size-full shrink-0 overflow-hidden rounded-full', '[&_[data-slot="avatar"]]:size-full', className )} style={{ width: size, height: size, maskImage: index ? `radial-gradient(circle ${size / 2}px at -${size / 4 + size / 10}px 50%, transparent 99%, white 100%)` : '', }} > {children} </div> );}type AvatarGroupTooltipProps = TooltipContentProps;function AvatarGroupTooltip(props: AvatarGroupTooltipProps) { return <TooltipContent {...props} />;}type AvatarGroupVariant = 'motion' | 'css' | 'stack';type AvatarGroupProps = Omit<React.ComponentProps<'div'>, 'translate'> & { children: React.ReactElement[]; variant?: AvatarGroupVariant; transition?: Transition; invertOverlap?: boolean; translate?: string | number; tooltipProps?: Partial<TooltipContentProps>; // Stack-specific props animate?: boolean; size?: number;};function AvatarGroup({ ref, children, className, variant = 'motion', transition = { type: 'spring', stiffness: 300, damping: 17 }, invertOverlap = false, translate = '-30%', tooltipProps = { side: 'top', sideOffset: 24 }, animate = false, size = 40, ...props}: AvatarGroupProps) { // Stack variant if (variant === 'stack') { return ( <div ref={ref} className={cn( '-space-x-1 flex items-center', animate && 'hover:space-x-0 [&>*]:transition-all', className )} {...props} > {Children.map(children, (child, index) => { if (!child) { return null; } return ( <AvatarStackItem key={index} index={index} size={size} className={className} > {child} </AvatarStackItem> ); })} </div> ); } // Motion and CSS variants with tooltips return ( <TooltipProvider delayDuration={0}> <div ref={ref} data-slot="avatar-group" className={cn( 'flex items-center', variant === 'css' && '-space-x-3', variant === 'motion' && 'flex-row -space-x-2 h-8', className )} {...props} > {children?.map((child, index) => { const zIndex = invertOverlap ? React.Children.count(children) - index : index; if (variant === 'motion') { return ( <AvatarMotionContainer key={index} zIndex={zIndex} translate={translate} transition={transition} tooltipProps={tooltipProps} > {child} </AvatarMotionContainer> ); } return ( <AvatarCSSContainer key={index} zIndex={zIndex} tooltipProps={tooltipProps} > {child} </AvatarCSSContainer> ); })} </div> </TooltipProvider> );}export { AvatarGroup, AvatarGroupTooltip, type AvatarGroupProps, type AvatarGroupTooltipProps, type AvatarGroupVariant,};
A sleek, CSS-only avatar group using TailwindCSS for the translate animation with hover effects.
Stack Variant
'use client';import { Avatar, AvatarFallback, AvatarImage,} from '@/components/ui/avatar';import { AvatarGroup,} from '@/components/ui/shadcn-io/avatar-group';const AVATARS = [ { src: 'https://github.com/haydenbleasel.png', fallback: 'HB', }, { src: 'https://github.com/shadcn.png', fallback: 'CN', }, { src: 'https://github.com/leerob.png', fallback: 'LR', }, { src: 'https://github.com/serafimcloud.png', fallback: 'SC', },];export const AvatarGroupStackDemo = () => { return ( <AvatarGroup variant="stack"> {AVATARS.map((avatar, index) => ( <Avatar key={index}> <AvatarImage src={avatar.src} /> <AvatarFallback>{avatar.fallback}</AvatarFallback> </Avatar> ))} </AvatarGroup> );};export default AvatarGroupStackDemo;
'use client';import * as React from 'react';import { motion, type Transition } from 'motion/react';import { Children, type ReactNode } from 'react';import { cn } from '@/lib/utils';import { TooltipContent, TooltipProvider, TooltipTrigger,} from '@/components/ui/tooltip';import * as TooltipPrimitive from '@radix-ui/react-tooltip';// Define types based on componentstype TooltipContentProps = React.ComponentProps<typeof TooltipContent>;// Avatar Container for motion-based interactionstype AvatarMotionProps = { children: React.ReactNode; zIndex: number; translate: string | number; transition: Transition; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarMotionContainer({ children, zIndex, translate, transition, tooltipContent, tooltipProps,}: AvatarMotionProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <motion.div data-slot="avatar-container" className="relative" style={{ zIndex }} whileHover={{ y: translate, }} transition={transition} > {children} </motion.div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for CSS-based interactionstype AvatarCSSProps = { children: React.ReactNode; zIndex: number; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarCSSContainer({ children, zIndex, tooltipContent, tooltipProps,}: AvatarCSSProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <div data-slot="avatar-container" className="relative transition-transform duration-300 ease-out hover:-translate-y-2" style={{ zIndex }} > {children} </div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for stack variant with masktype AvatarStackItemProps = { children: React.ReactNode; index: number; size: number; className?: string;};function AvatarStackItem({ children, index, size, className }: AvatarStackItemProps) { return ( <div className={cn( 'size-full shrink-0 overflow-hidden rounded-full', '[&_[data-slot="avatar"]]:size-full', className )} style={{ width: size, height: size, maskImage: index ? `radial-gradient(circle ${size / 2}px at -${size / 4 + size / 10}px 50%, transparent 99%, white 100%)` : '', }} > {children} </div> );}type AvatarGroupTooltipProps = TooltipContentProps;function AvatarGroupTooltip(props: AvatarGroupTooltipProps) { return <TooltipContent {...props} />;}type AvatarGroupVariant = 'motion' | 'css' | 'stack';type AvatarGroupProps = Omit<React.ComponentProps<'div'>, 'translate'> & { children: React.ReactElement[]; variant?: AvatarGroupVariant; transition?: Transition; invertOverlap?: boolean; translate?: string | number; tooltipProps?: Partial<TooltipContentProps>; // Stack-specific props animate?: boolean; size?: number;};function AvatarGroup({ ref, children, className, variant = 'motion', transition = { type: 'spring', stiffness: 300, damping: 17 }, invertOverlap = false, translate = '-30%', tooltipProps = { side: 'top', sideOffset: 24 }, animate = false, size = 40, ...props}: AvatarGroupProps) { // Stack variant if (variant === 'stack') { return ( <div ref={ref} className={cn( '-space-x-1 flex items-center', animate && 'hover:space-x-0 [&>*]:transition-all', className )} {...props} > {Children.map(children, (child, index) => { if (!child) { return null; } return ( <AvatarStackItem key={index} index={index} size={size} className={className} > {child} </AvatarStackItem> ); })} </div> ); } // Motion and CSS variants with tooltips return ( <TooltipProvider delayDuration={0}> <div ref={ref} data-slot="avatar-group" className={cn( 'flex items-center', variant === 'css' && '-space-x-3', variant === 'motion' && 'flex-row -space-x-2 h-8', className )} {...props} > {children?.map((child, index) => { const zIndex = invertOverlap ? React.Children.count(children) - index : index; if (variant === 'motion') { return ( <AvatarMotionContainer key={index} zIndex={zIndex} translate={translate} transition={transition} tooltipProps={tooltipProps} > {child} </AvatarMotionContainer> ); } return ( <AvatarCSSContainer key={index} zIndex={zIndex} tooltipProps={tooltipProps} > {child} </AvatarCSSContainer> ); })} </div> </TooltipProvider> );}export { AvatarGroup, AvatarGroupTooltip, type AvatarGroupProps, type AvatarGroupTooltipProps, type AvatarGroupVariant,};
Classic stacked avatar layout with mask overlay effects for a clean, layered appearance.
Stack Animated
'use client';import { Avatar, AvatarFallback, AvatarImage,} from '@/components/ui/avatar';import { AvatarGroup,} from '@/components/ui/shadcn-io/avatar-group';const AVATARS = [ { src: 'https://github.com/haydenbleasel.png', fallback: 'HB', }, { src: 'https://github.com/shadcn.png', fallback: 'CN', }, { src: 'https://github.com/leerob.png', fallback: 'LR', }, { src: 'https://github.com/serafimcloud.png', fallback: 'SC', },];export const AvatarGroupStackAnimatedDemo = () => { return ( <AvatarGroup variant="stack" animate> {AVATARS.map((avatar, index) => ( <Avatar key={index}> <AvatarImage src={avatar.src} /> <AvatarFallback>{avatar.fallback}</AvatarFallback> </Avatar> ))} </AvatarGroup> );};export default AvatarGroupStackAnimatedDemo;
'use client';import * as React from 'react';import { motion, type Transition } from 'motion/react';import { Children, type ReactNode } from 'react';import { cn } from '@/lib/utils';import { TooltipContent, TooltipProvider, TooltipTrigger,} from '@/components/ui/tooltip';import * as TooltipPrimitive from '@radix-ui/react-tooltip';// Define types based on componentstype TooltipContentProps = React.ComponentProps<typeof TooltipContent>;// Avatar Container for motion-based interactionstype AvatarMotionProps = { children: React.ReactNode; zIndex: number; translate: string | number; transition: Transition; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarMotionContainer({ children, zIndex, translate, transition, tooltipContent, tooltipProps,}: AvatarMotionProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <motion.div data-slot="avatar-container" className="relative" style={{ zIndex }} whileHover={{ y: translate, }} transition={transition} > {children} </motion.div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for CSS-based interactionstype AvatarCSSProps = { children: React.ReactNode; zIndex: number; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarCSSContainer({ children, zIndex, tooltipContent, tooltipProps,}: AvatarCSSProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <div data-slot="avatar-container" className="relative transition-transform duration-300 ease-out hover:-translate-y-2" style={{ zIndex }} > {children} </div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for stack variant with masktype AvatarStackItemProps = { children: React.ReactNode; index: number; size: number; className?: string;};function AvatarStackItem({ children, index, size, className }: AvatarStackItemProps) { return ( <div className={cn( 'size-full shrink-0 overflow-hidden rounded-full', '[&_[data-slot="avatar"]]:size-full', className )} style={{ width: size, height: size, maskImage: index ? `radial-gradient(circle ${size / 2}px at -${size / 4 + size / 10}px 50%, transparent 99%, white 100%)` : '', }} > {children} </div> );}type AvatarGroupTooltipProps = TooltipContentProps;function AvatarGroupTooltip(props: AvatarGroupTooltipProps) { return <TooltipContent {...props} />;}type AvatarGroupVariant = 'motion' | 'css' | 'stack';type AvatarGroupProps = Omit<React.ComponentProps<'div'>, 'translate'> & { children: React.ReactElement[]; variant?: AvatarGroupVariant; transition?: Transition; invertOverlap?: boolean; translate?: string | number; tooltipProps?: Partial<TooltipContentProps>; // Stack-specific props animate?: boolean; size?: number;};function AvatarGroup({ ref, children, className, variant = 'motion', transition = { type: 'spring', stiffness: 300, damping: 17 }, invertOverlap = false, translate = '-30%', tooltipProps = { side: 'top', sideOffset: 24 }, animate = false, size = 40, ...props}: AvatarGroupProps) { // Stack variant if (variant === 'stack') { return ( <div ref={ref} className={cn( '-space-x-1 flex items-center', animate && 'hover:space-x-0 [&>*]:transition-all', className )} {...props} > {Children.map(children, (child, index) => { if (!child) { return null; } return ( <AvatarStackItem key={index} index={index} size={size} className={className} > {child} </AvatarStackItem> ); })} </div> ); } // Motion and CSS variants with tooltips return ( <TooltipProvider delayDuration={0}> <div ref={ref} data-slot="avatar-group" className={cn( 'flex items-center', variant === 'css' && '-space-x-3', variant === 'motion' && 'flex-row -space-x-2 h-8', className )} {...props} > {children?.map((child, index) => { const zIndex = invertOverlap ? React.Children.count(children) - index : index; if (variant === 'motion') { return ( <AvatarMotionContainer key={index} zIndex={zIndex} translate={translate} transition={transition} tooltipProps={tooltipProps} > {child} </AvatarMotionContainer> ); } return ( <AvatarCSSContainer key={index} zIndex={zIndex} tooltipProps={tooltipProps} > {child} </AvatarCSSContainer> ); })} </div> </TooltipProvider> );}export { AvatarGroup, AvatarGroupTooltip, type AvatarGroupProps, type AvatarGroupTooltipProps, type AvatarGroupVariant,};
Enhanced stack variant with hover animations that expand the spacing between avatars.
Bottom Tooltips
'use client';import { Avatar, AvatarFallback, AvatarImage,} from '@/components/ui/avatar';import { AvatarGroup, AvatarGroupTooltip,} from '@/components/ui/shadcn-io/avatar-group'; const AVATARS = [ { src: 'https://pbs.twimg.com/profile_images/1909615404789506048/MTqvRsjo_400x400.jpg', fallback: 'SK', tooltip: 'Skyleen', }, { src: 'https://pbs.twimg.com/profile_images/1593304942210478080/TUYae5z7_400x400.jpg', fallback: 'CN', tooltip: 'Shadcn', }, { src: 'https://pbs.twimg.com/profile_images/1677042510839857154/Kq4tpySA_400x400.jpg', fallback: 'AW', tooltip: 'Adam Wathan', }, { src: 'https://pbs.twimg.com/profile_images/1783856060249595904/8TfcCN0r_400x400.jpg', fallback: 'GR', tooltip: 'Guillermo Rauch', }, { src: 'https://pbs.twimg.com/profile_images/1534700564810018816/anAuSfkp_400x400.jpg', fallback: 'JH', tooltip: 'Jhey', },]; export const AvatarGroupBottomDemo = () => { return ( <div className="bg-gradient-to-r from-indigo-500 from-10% via-sky-500 via-30% to-emerald-500 to-90% p-0.5 rounded-full"> <div className="bg-gradient-to-r from-indigo-100 dark:from-indigo-950 from-10% via-sky-100 dark:via-sky-950 via-30% to-emerald-100 dark:to-emerald-950 to-90% p-1.5 rounded-full"> <AvatarGroup variant="css" invertOverlap tooltipProps={{ side: 'bottom', sideOffset: 12 }} > {AVATARS.map((avatar, index) => ( <Avatar key={index}> <AvatarImage src={avatar.src} /> <AvatarFallback>{avatar.fallback}</AvatarFallback> <AvatarGroupTooltip> <p>{avatar.tooltip}</p> </AvatarGroupTooltip> </Avatar> ))} </AvatarGroup> </div> </div> );};export default AvatarGroupBottomDemo;
'use client';import * as React from 'react';import { motion, type Transition } from 'motion/react';import { Children, type ReactNode } from 'react';import { cn } from '@/lib/utils';import { TooltipContent, TooltipProvider, TooltipTrigger,} from '@/components/ui/tooltip';import * as TooltipPrimitive from '@radix-ui/react-tooltip';// Define types based on componentstype TooltipContentProps = React.ComponentProps<typeof TooltipContent>;// Avatar Container for motion-based interactionstype AvatarMotionProps = { children: React.ReactNode; zIndex: number; translate: string | number; transition: Transition; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarMotionContainer({ children, zIndex, translate, transition, tooltipContent, tooltipProps,}: AvatarMotionProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <motion.div data-slot="avatar-container" className="relative" style={{ zIndex }} whileHover={{ y: translate, }} transition={transition} > {children} </motion.div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for CSS-based interactionstype AvatarCSSProps = { children: React.ReactNode; zIndex: number; tooltipContent?: React.ReactNode; tooltipProps?: Partial<TooltipContentProps>;};function AvatarCSSContainer({ children, zIndex, tooltipContent, tooltipProps,}: AvatarCSSProps) { return ( <TooltipPrimitive.Root> <TooltipTrigger> <div data-slot="avatar-container" className="relative transition-transform duration-300 ease-out hover:-translate-y-2" style={{ zIndex }} > {children} </div> </TooltipTrigger> {tooltipContent && ( <AvatarGroupTooltip {...tooltipProps}> {tooltipContent} </AvatarGroupTooltip> )} </TooltipPrimitive.Root> );}// Avatar Container for stack variant with masktype AvatarStackItemProps = { children: React.ReactNode; index: number; size: number; className?: string;};function AvatarStackItem({ children, index, size, className }: AvatarStackItemProps) { return ( <div className={cn( 'size-full shrink-0 overflow-hidden rounded-full', '[&_[data-slot="avatar"]]:size-full', className )} style={{ width: size, height: size, maskImage: index ? `radial-gradient(circle ${size / 2}px at -${size / 4 + size / 10}px 50%, transparent 99%, white 100%)` : '', }} > {children} </div> );}type AvatarGroupTooltipProps = TooltipContentProps;function AvatarGroupTooltip(props: AvatarGroupTooltipProps) { return <TooltipContent {...props} />;}type AvatarGroupVariant = 'motion' | 'css' | 'stack';type AvatarGroupProps = Omit<React.ComponentProps<'div'>, 'translate'> & { children: React.ReactElement[]; variant?: AvatarGroupVariant; transition?: Transition; invertOverlap?: boolean; translate?: string | number; tooltipProps?: Partial<TooltipContentProps>; // Stack-specific props animate?: boolean; size?: number;};function AvatarGroup({ ref, children, className, variant = 'motion', transition = { type: 'spring', stiffness: 300, damping: 17 }, invertOverlap = false, translate = '-30%', tooltipProps = { side: 'top', sideOffset: 24 }, animate = false, size = 40, ...props}: AvatarGroupProps) { // Stack variant if (variant === 'stack') { return ( <div ref={ref} className={cn( '-space-x-1 flex items-center', animate && 'hover:space-x-0 [&>*]:transition-all', className )} {...props} > {Children.map(children, (child, index) => { if (!child) { return null; } return ( <AvatarStackItem key={index} index={index} size={size} className={className} > {child} </AvatarStackItem> ); })} </div> ); } // Motion and CSS variants with tooltips return ( <TooltipProvider delayDuration={0}> <div ref={ref} data-slot="avatar-group" className={cn( 'flex items-center', variant === 'css' && '-space-x-3', variant === 'motion' && 'flex-row -space-x-2 h-8', className )} {...props} > {children?.map((child, index) => { const zIndex = invertOverlap ? React.Children.count(children) - index : index; if (variant === 'motion') { return ( <AvatarMotionContainer key={index} zIndex={zIndex} translate={translate} transition={transition} tooltipProps={tooltipProps} > {child} </AvatarMotionContainer> ); } return ( <AvatarCSSContainer key={index} zIndex={zIndex} tooltipProps={tooltipProps} > {child} </AvatarCSSContainer> ); })} </div> </TooltipProvider> );}export { AvatarGroup, AvatarGroupTooltip, type AvatarGroupProps, type AvatarGroupTooltipProps, type AvatarGroupVariant,};
Avatar group with tooltips positioned at the bottom and inverted overlap order.
Use Cases
This free open source React component works well for:
- Team displays - Show team members or collaborators built with Next.js
- User lists - Display active users or participants using TypeScript and Tailwind CSS
- Social features - Friend lists and community members with shadcn/ui integration
- Collaboration tools - Show who's working on projects for React applications
- Comment sections - Display comment authors with animated interactions
API Reference
AvatarGroup
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactElement[] | required | Array of Avatar components to display |
variant | 'motion' | 'css' | 'stack' | 'motion' | Display variant for the avatar group |
transition | Transition | { type: 'spring', stiffness: 300, damping: 17 } | Motion transition configuration (motion variant only) |
invertOverlap | boolean | false | Reverse z-index stacking order |
translate | string | number | '-30%' | Hover translate distance (motion/css variants) |
tooltipProps | TooltipProps | { side: 'top', sideOffset: 24 } | Tooltip positioning and behavior |
animate | boolean | false | Enable hover spacing animation (stack variant only) |
size | number | 40 | Avatar size in pixels (stack variant only) |
className | string | - | Additional CSS classes for container |
AvatarGroupTooltip
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactNode | required | Tooltip content to display |
Variants
Motion Variant
Uses Framer Motion for smooth spring-based hover animations with customizable physics.
<AvatarGroup variant="motion" transition={{ type: 'spring', stiffness: 400 }}>
{/* Avatar components */}
</AvatarGroup>
CSS Variant
Pure CSS hover effects with TailwindCSS transitions for lightweight performance.
<AvatarGroup variant="css">
{/* Avatar components */}
</AvatarGroup>
Stack Variant
Traditional stacked layout with mask overlay effects and optional hover animations.
<AvatarGroup variant="stack" animate size={48}>
{/* Avatar components */}
</AvatarGroup>
Motion Configuration
transition?: {
type?: 'spring' | 'tween';
stiffness?: number; // Spring stiffness (default: 300)
damping?: number; // Spring damping (default: 17)
duration?: number; // Animation duration
ease?: string | number[]; // Easing function
}
Tooltip Props
tooltipProps?: {
side?: 'top' | 'right' | 'bottom' | 'left';
sideOffset?: number;
align?: 'start' | 'center' | 'end';
openDelay?: number;
closeDelay?: number;
}
Implementation Notes
- Motion variant: Uses Framer Motion for smooth hover animations with spring physics
- CSS variant: Uses TailwindCSS transitions for lightweight hover effects
- Stack variant: Uses CSS mask properties for clean overlay effects
- Integrates with shadcn/ui tooltip system for consistent behavior
- Automatically manages z-index stacking for proper avatar layering
- Supports both percentage and pixel values for translate distances
- Works with any shadcn/ui Avatar component configuration
- TooltipProvider handles timing and interaction behavior (motion/css variants only)
- Hover effects trigger on both hover and tap for touch devices
- Compatible with custom avatar sizes and border configurations
- Integrates seamlessly with shadcn/ui design system patterns
Marquee
Horizontal scrolling marquee component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring smooth animations, customizable spacing, and fade effects.
Pill
Flexible badge pill component for React and Next.js applications. Built with TypeScript support, Tailwind CSS styling, and shadcn/ui design featuring status indicators, avatar integration, and customizable variants.