Avatar Group
Stacked avatars with presence dots and a +N overflow that lists the rest.
@drift-ui/avatar-groupRegistry JSONAna Lima (online)Kenji Sato (busy)Priya Nair (away)Tomás Ruiz
On this page
Installation
The CLI also installs the npm packages and registry items it builds on. Register the namespace once first with https://drift-ui.dev/r/{style}/{name}.json. The CLI fills in {style} from your components.json and installs the Base UI or Radix build to match. See setup.
API reference
AvatarGroup
Stacks its AvatarGroupItem children with overlap. Avatars past max collapse into a +N button that opens a popover listing them. Pass aria-label.
| Prop | Type | Default |
|---|---|---|
maxnumberMost avatars shown before the +N button. | number |
Other props go to the <div>.
AvatarGroupItem
One avatar built on the shadcn Avatar, with initials as the fallback and an optional presence dot.
| Prop | Type | Default |
|---|---|---|
nameRequiredstringUsed for the initials, the tooltip and the screen reader label. | string | |
srcstringImage URL. | string | |
status"online" | "away" | "busy" | "offline"Adds a presence dot and reads the status aloud. | "online" | "away" | "busy" | "offline" |
Other props go to the <span>.
Source
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
type AvatarStatus = "online" | "away" | "busy" | "offline"
type AvatarGroupItemProps = React.ComponentProps<"span"> & {
src?: string
name: string
status?: AvatarStatus
}
const STATUS_CLASSES: Record<AvatarStatus, string> = {
online: "bg-chart-2",
away: "border-2 border-chart-2 bg-background",
busy: "bg-destructive",
offline: "bg-muted-foreground",
}
function initialsOf(name: string) {
return name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((word) => word[0])
.join("")
.toUpperCase()
}
function AvatarGroupItem({
src,
name,
status,
className,
...props
}: AvatarGroupItemProps) {
return (
<span
data-slot="avatar-group-item"
title={name}
className={cn("relative inline-flex size-8 shrink-0", className)}
{...props}
>
<Avatar aria-hidden className="size-full ring-2 ring-background">
<AvatarImage src={src} alt="" />
<AvatarFallback className="text-xs font-medium">
{initialsOf(name)}
</AvatarFallback>
</Avatar>
{status && (
<span
aria-hidden
data-status={status}
className={cn(
"absolute -right-0.5 -bottom-0.5 size-2.5 rounded-full ring-2 ring-background",
STATUS_CLASSES[status]
)}
/>
)}
<span className="sr-only">{status ? `${name} (${status})` : name}</span>
</span>
)
}
function AvatarGroup({
max,
className,
children,
...props
}: React.ComponentProps<"div"> & { max?: number }) {
const items = React.Children.toArray(children).filter(
(child): child is React.ReactElement<AvatarGroupItemProps> =>
React.isValidElement(child)
)
const visible = max === undefined ? items : items.slice(0, max)
const hidden = items.slice(visible.length)
return (
<div
role="group"
data-slot="avatar-group"
className={cn("flex items-center -space-x-1.5", className)}
{...props}
>
{visible.map((item, index) =>
React.cloneElement(item, {
style: { zIndex: visible.length - index, ...item.props.style },
})
)}
{hidden.length > 0 && (
<Popover>
<PopoverTrigger
data-slot="avatar-group-overflow"
aria-label={`${hidden.length} more`}
className="relative inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium ring-2 ring-background outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
+{hidden.length}
</PopoverTrigger>
<PopoverContent
align="start"
aria-label={`${hidden.length} more`}
className="w-56 p-1"
>
<ul className="grid max-h-64 overflow-y-auto">
{hidden.map((item) => (
<li
key={item.key}
className="flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm"
>
{React.cloneElement(item, {
"aria-hidden": true,
className: cn(item.props.className, "size-7"),
})}
<span className="truncate">{item.props.name}</span>
{item.props.status && (
<span className="ml-auto text-xs text-muted-foreground capitalize">
{item.props.status}
</span>
)}
</li>
))}
</ul>
</PopoverContent>
</Popover>
)}
</div>
)
}
export { AvatarGroup, AvatarGroupItem, type AvatarStatus }