Docs
Go back
Component by
OV
Oblivion+2
TSX
avatar-stack
"use client"
import Image from "next/image"
import { cn } from "@/lib/utils"
export const componentMeta = {
name: "Avatar Stack",
description: "Overlapping circular avatars with an overflow count badge. Great for social proof sections.",
props: {
avatars: { type: "Array<{ src: string; alt: string }>", description: "Avatar image objects" },
max: { type: "number", default: "4", description: "Maximum avatars shown before +N badge" },
size: { type: "'sm' | 'md' | 'lg'", default: '"md"', description: "Avatar size" },
label: { type: "string", description: "Optional label displayed next to the stack" },
}
}
type Size = "sm" | "md" | "lg"
const SIZE_CLS: Record<Size, string> = {
sm: "w-7 h-7 text-[10px]",
md: "w-9 h-9 text-xs",
lg: "w-12 h-12 text-sm",
}
const OFFSET_CLS: Record<Size, string> = {
sm: "-ml-2",
md: "-ml-2.5",
lg: "-ml-3",
}
// Unsplash License — free for commercial use, no attribution required
const DEFAULT_AVATARS = [
{ src: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=80&h=80&fit=crop&crop=face", alt: "Person 1" },
{ src: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=80&h=80&fit=crop&crop=face", alt: "Person 2" },
{ src: "https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=80&h=80&fit=crop&crop=face", alt: "Person 3" },
{ src: "https://images.unsplash.com/photo-1499996860823-5214fcc65f8f?w=80&h=80&fit=crop&crop=face", alt: "Person 4" },
{ src: "https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?w=80&h=80&fit=crop&crop=face", alt: "Person 5" },
{ src: "https://images.unsplash.com/photo-1463453091185-61582044d556?w=80&h=80&fit=crop&crop=face", alt: "Person 6" },
]
export default function AvatarStack({
avatars = DEFAULT_AVATARS,
max = 4,
size = "md",
label,
className,
}: {
avatars?: { src: string; alt: string }[]
max?: number
size?: Size
label?: string
className?: string
}) {
const shown = avatars.slice(0, max)
const overflow = avatars.length - max
return (
<div className={cn("flex items-center gap-3", className)}>
<div className="flex items-center">
{shown.map((av, i) => (
<div
key={i}
className={cn(
"relative rounded-full ring-2 ring-white dark:ring-zinc-950 overflow-hidden shrink-0",
SIZE_CLS[size],
i > 0 && OFFSET_CLS[size]
)}
>
<Image
src={av.src}
alt={av.alt}
fill
className="object-cover"
sizes="80px"
unoptimized
/>
</div>
))}
{overflow > 0 && (
<div
className={cn(
"flex items-center justify-center rounded-full ring-2 ring-white dark:ring-zinc-950",
"bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 font-semibold",
SIZE_CLS[size],
OFFSET_CLS[size]
)}
>
+{overflow}
</div>
)}
</div>
{label && (
<span className="text-sm text-zinc-600 dark:text-zinc-400 font-medium">{label}</span>
)}
</div>
)
}
export function AvatarStackPreview() {
return (
<div className="flex h-full min-h-[480px] w-full flex-col items-center justify-center gap-10 bg-transparent">
<div className="text-center space-y-1">
<p className="text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400">Trusted by</p>
<p className="text-2xl font-bold text-zinc-900 dark:text-white">12,000+ developers</p>
</div>
<div className="flex flex-col items-center gap-6">
<AvatarStack size="lg" label="12k+ developers trust Oblivion UI" />
<AvatarStack size="md" max={3} label="Join the community" />
<AvatarStack size="sm" max={5} />
</div>
</div>
)
}"use client"
import Image from "next/image"
import { cn } from "@/lib/utils"
export const componentMeta = {
name: "Avatar Stack",
description: "Overlapping circular avatars with an overflow count badge. Great for social proof sections.",
props: {
avatars: { type: "Array<{ src: string; alt: string }>", description: "Avatar image objects" },
max: { type: "number", default: "4", description: "Maximum avatars shown before +N badge" },
size: { type: "'sm' | 'md' | 'lg'", default: '"md"', description: "Avatar size" },
label: { type: "string", description: "Optional label displayed next to the stack" },
}
}
type Size = "sm" | "md" | "lg"
const SIZE_CLS: Record<Size, string> = {
sm: "w-7 h-7 text-[10px]",
md: "w-9 h-9 text-xs",
lg: "w-12 h-12 text-sm",
}
const OFFSET_CLS: Record<Size, string> = {
sm: "-ml-2",
md: "-ml-2.5",
lg: "-ml-3",
}
// Unsplash License — free for commercial use, no attribution required
const DEFAULT_AVATARS = [
{ src: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=80&h=80&fit=crop&crop=face", alt: "Person 1" },
{ src: "https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=80&h=80&fit=crop&crop=face", alt: "Person 2" },
{ src: "https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=80&h=80&fit=crop&crop=face", alt: "Person 3" },
{ src: "https://images.unsplash.com/photo-1499996860823-5214fcc65f8f?w=80&h=80&fit=crop&crop=face", alt: "Person 4" },
{ src: "https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?w=80&h=80&fit=crop&crop=face", alt: "Person 5" },
{ src: "https://images.unsplash.com/photo-1463453091185-61582044d556?w=80&h=80&fit=crop&crop=face", alt: "Person 6" },
]
export default function AvatarStack({
avatars = DEFAULT_AVATARS,
max = 4,
size = "md",
label,
className,
}: {
avatars?: { src: string; alt: string }[]
max?: number
size?: Size
label?: string
className?: string
}) {
const shown = avatars.slice(0, max)
const overflow = avatars.length - max
return (
<div className={cn("flex items-center gap-3", className)}>
<div className="flex items-center">
{shown.map((av, i) => (
<div
key={i}
className={cn(
"relative rounded-full ring-2 ring-white dark:ring-zinc-950 overflow-hidden shrink-0",
SIZE_CLS[size],
i > 0 && OFFSET_CLS[size]
)}
>
<Image
src={av.src}
alt={av.alt}
fill
className="object-cover"
sizes="80px"
unoptimized
/>
</div>
))}
{overflow > 0 && (
<div
className={cn(
"flex items-center justify-center rounded-full ring-2 ring-white dark:ring-zinc-950",
"bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 font-semibold",
SIZE_CLS[size],
OFFSET_CLS[size]
)}
>
+{overflow}
</div>
)}
</div>
{label && (
<span className="text-sm text-zinc-600 dark:text-zinc-400 font-medium">{label}</span>
)}
</div>
)
}
export function AvatarStackPreview() {
return (
<div className="flex h-full min-h-[480px] w-full flex-col items-center justify-center gap-10 bg-transparent">
<div className="text-center space-y-1">
<p className="text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400">Trusted by</p>
<p className="text-2xl font-bold text-zinc-900 dark:text-white">12,000+ developers</p>
</div>
<div className="flex flex-col items-center gap-6">
<AvatarStack size="lg" label="12k+ developers trust Oblivion UI" />
<AvatarStack size="md" max={3} label="Join the community" />
<AvatarStack size="sm" max={5} />
</div>
</div>
)
}Save to favoritesCopy to Figma
React
Avatar Stack
Overlapping circular avatars with an overflow count badge. Great for social proof sections.
Installation
$ npx @cosmoo/oblivion add avatar-stackDependencies
npm install Usage Example
tsx
import AvatarStack from "@/components/oblivion/avatar-stack"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<AvatarStack />
</div>
)
}import AvatarStack from "@/components/oblivion/avatar-stack"
export default function Demo() {
return (
<div className="p-10 flex justify-center">
<AvatarStack />
</div>
)
}