Avatar
An image element with a fallback for representing the user. Built with Base UI and Tailwind CSS. Copy-paste ready.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
export default function Particle() {
return (
<Avatar>
<AvatarImage
alt="Luke Tracy"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80"
/>
<AvatarFallback>LT</AvatarFallback>
</Avatar>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/avatar
Usage
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"<Avatar>
<AvatarImage src="/avatars/01.png" alt="User avatar" />
<AvatarFallback>JD</AvatarFallback>
</Avatar>Examples
With Fallback
Shows the AvatarFallback with initials when no image URL is provided or the image fails to load.
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
export default function Particle() {
return (
<Avatar>
<AvatarFallback>LT</AvatarFallback>
</Avatar>
);
}
Different Sizes
Renders avatars at multiple sizes to show how the component scales across layout and density contexts.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
export default function Particle() {
return (
<div className="flex items-center gap-4">
<Avatar>
<AvatarImage
alt="User"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>AV</AvatarFallback>
</Avatar>
<Avatar className="size-12">
<AvatarImage
alt="User"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=144&h=144&dpr=2&q=80"
/>
<AvatarFallback>AV</AvatarFallback>
</Avatar>
<Avatar className="size-16">
<AvatarImage
alt="User"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=192&h=192&dpr=2&q=80"
/>
<AvatarFallback>AV</AvatarFallback>
</Avatar>
</div>
);
}
Different Radius
Demonstrates square, rounded, and fully circular border radius variants for adapting the avatar to different design styles.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
export default function Particle() {
return (
<div className="flex items-center gap-4">
<Avatar className="rounded-md">
<AvatarImage
alt="User"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80"
/>
<AvatarFallback>AV</AvatarFallback>
</Avatar>
<Avatar className="rounded-xl">
<AvatarImage
alt="User"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80"
/>
<AvatarFallback>AV</AvatarFallback>
</Avatar>
<Avatar className="rounded-full">
<AvatarImage
alt="User"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80"
/>
<AvatarFallback>AV</AvatarFallback>
</Avatar>
</div>
);
}
Avatar Group
Stacks multiple avatars with overlapping offsets to represent a collection of users in a compact row.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
export default function Particle() {
return (
<div className="flex space-x-[-0.6rem]">
<Avatar className="ring-2 ring-background">
<AvatarImage
alt="U1"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>U1</AvatarFallback>
</Avatar>
<Avatar className="ring-2 ring-background">
<AvatarImage
alt="U2"
src="https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>U2</AvatarFallback>
</Avatar>
<Avatar className="ring-2 ring-background">
<AvatarImage
alt="U3"
src="https://images.unsplash.com/photo-1655874819398-c6dfbec68ac7?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>U3</AvatarFallback>
</Avatar>
</div>
);
}
Group With Count
Adds a numbered overflow indicator (+N) to an avatar group when the total count exceeds the visible limit.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
export function Pattern() {
return (
<div className="flex space-x-[-0.6rem]">
<Avatar className="ring-2 ring-background">
<AvatarImage
alt="Sarah Chen"
src="https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>SC</AvatarFallback>
</Avatar>
<Avatar className="ring-2 ring-background">
<AvatarImage
alt="Michael Rodriguez"
src="https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>MR</AvatarFallback>
</Avatar>
<Avatar className="ring-2 ring-background">
<AvatarImage
alt="Emma Wilson"
src="https://images.unsplash.com/photo-1485893086445-ed75865251e0?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>EW</AvatarFallback>
</Avatar>
<Avatar className="ring-2 ring-background">
<AvatarFallback>+3</AvatarFallback>
</Avatar>
</div>
);
}
With Details and Badge
Pairs the avatar with a name and email label alongside a status badge for user profile list rows.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
export function Pattern() {
return (
<div className="flex items-center gap-1.5">
<Avatar>
<AvatarImage
alt="Alex Johnson"
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>AJ</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<div className="flex items-center gap-1.5">
<span className="font-semibold text-sm">Alex Johnson</span>
<Badge size="sm" variant="default">
Pro
</Badge>
</div>
<span className="text-muted-foreground text-xs">Founder & CEO</span>
</div>
</div>
);
}
Social Proof
Shows a row of overlapping avatars beside a short text label for social proof patterns like "Trusted by 200+ teams".
Trusted by 100K+ users.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
export function Pattern() {
return (
<div className="flex items-center gap-1.5 rounded-full border border-border p-1 shadow-black/5 shadow-sm">
<div className="flex space-x-[-0.6rem]">
<Avatar className="size-7 ring-2 ring-background">
<AvatarImage
alt="Liam Thompson"
src="https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>LT</AvatarFallback>
</Avatar>
<Avatar className="size-7 ring-2 ring-background">
<AvatarImage
alt="Nick Johnson"
src="https://images.unsplash.com/photo-1485206412256-701ccc5b93ca?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>NJ</AvatarFallback>
</Avatar>
<Avatar className="size-7 ring-2 ring-background">
<AvatarImage
alt="Maria Garcia"
src="https://images.unsplash.com/photo-1620075225255-8c2051b6c015?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>MG</AvatarFallback>
</Avatar>
<Avatar className="size-7 ring-2 ring-background">
<AvatarImage alt="@leerob" src="https://github.com/leerob.png" />
<AvatarFallback>CH</AvatarFallback>
</Avatar>
</div>
<p className="me-1.5 text-muted-foreground text-xs">
Trusted by <span className="font-semibold text-foreground">100K+</span>{" "}
users.
</p>
</div>
);
}
Social Proof Compact
A tighter social proof strip using initials-only fallback avatars for a minimal footprint below a CTA.
Joined by 500+ developers.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Frame, FramePanel } from "@/components/ui/frame";
export function Pattern() {
return (
<Frame>
<FramePanel className="flex items-center gap-2 p-2!">
<div className="flex space-x-[-0.6rem]">
<Avatar className="size-7 ring-2 ring-background">
<AvatarImage alt="@shadcn" src="https://github.com/shadcn.png" />
<AvatarFallback>CH</AvatarFallback>
</Avatar>
<Avatar className="size-7 ring-2 ring-background">
<AvatarImage
alt="@maxleiter"
src="https://github.com/maxleiter.png"
/>
<AvatarFallback>CH</AvatarFallback>
</Avatar>
<Avatar className="size-7 ring-2 ring-background">
<AvatarImage
alt="@evilrabbit"
src="https://github.com/evilrabbit.png"
/>
<AvatarFallback>CH</AvatarFallback>
</Avatar>
<Avatar className="size-7 ring-2 ring-background">
<AvatarImage alt="@leerob" src="https://github.com/leerob.png" />
<AvatarFallback>CH</AvatarFallback>
</Avatar>
</div>
<p className="me-1.5 text-muted-foreground text-xs">
Joined by <span className="font-semibold text-foreground">500+</span>{" "}
developers.
</p>
</FramePanel>
</Frame>
);
}
With Hover Effect
Avatars in a group spread apart on hover to reveal individual images that would otherwise be obscured by overlap.
//biome-ignore-all lint/suspicious/noArrayIndexKey: <>
import { cn } from "@/lib/utils";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
const avatars = [
{
fallback: "DK",
name: "David Kim",
src: "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
},
{
fallback: "ML",
name: "Max Leiter",
src: "https://github.com/maxleiter.png",
},
{
fallback: "ER",
name: "James Brown",
src: "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
},
{
fallback: "JW",
name: "Jenny Wilson",
src: "https://github.com/pranathip.png",
},
];
export function Pattern() {
return (
<div className="group/avatars flex items-center px-2 py-4">
{avatars.map((avatar, index) => (
<div
className="group/avatar-item translate-x-[calc(var(--index)*-8px)] transition-all duration-300 ease-in-out will-change-transform group-hover/avatars:translate-x-[calc(var(--index)*6px)]"
key={index}
style={
{
"--index": index,
zIndex: avatars.length - index,
} as React.CSSProperties
}
>
<Avatar
className={cn(
"origin-center ring-2 ring-background transition-transform duration-300 ease-in-out",
"group-hover/avatar-item:scale-110",
)}
>
<AvatarImage alt={avatar.name} src={avatar.src} />
<AvatarFallback className="text-xs">
{avatar.fallback}
</AvatarFallback>
</Avatar>
</div>
))}
</div>
);
}
With Tooltips
Combines the hover spread effect with a Tooltip showing each user's name when the avatar is focused or hovered.
//biome-ignore-all lint/suspicious/noArrayIndexKey: <>
import { cn } from "@/lib/utils";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
const avatars = [
{
fallback: "DK",
name: "David Kim",
src: "https://images.unsplash.com/photo-1607990281513-2c110a25bd8c?w=96&h=96&dpr=2&q=80",
},
{
fallback: "ML",
name: "Max Leiter",
src: "https://github.com/maxleiter.png",
},
{
fallback: "ER",
name: "James Brown",
src: "https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80",
},
{
fallback: "JW",
name: "Jenny Wilson",
src: "https://github.com/pranathip.png",
},
];
export function Pattern() {
return (
<div className="group/avatars flex items-center px-2 py-4">
{avatars.map((avatar, index) => (
<div
className="group/avatar-item translate-x-[calc(var(--index)*-8px)] transition-all duration-300 ease-in-out will-change-transform group-hover/avatars:translate-x-[calc(var(--index)*6px)]"
key={index}
style={
{
"--index": index,
zIndex: avatars.length - index,
} as React.CSSProperties
}
>
<Tooltip>
<TooltipTrigger
render={
<Avatar
className={cn(
"origin-center ring-2 ring-background transition-transform duration-300 ease-in-out",
"group-hover/avatar-item:scale-110",
)}
>
<AvatarImage alt={avatar.name} src={avatar.src} />
<AvatarFallback className="text-xs">
{avatar.fallback}
</AvatarFallback>
</Avatar>
}
/>
<TooltipContent sideOffset={10}>{avatar.name}</TooltipContent>
</Tooltip>
</div>
))}
</div>
);
}
In Empty State
An empty state panel centered around an avatar placeholder, useful for "no members yet" or "invite your team" views.
No active collaborators
Invite teammates to start working together.
import { UserPlusIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
export function Pattern() {
return (
<div className="flex flex-col items-center gap-4 text-center">
<div className="flex space-x-[-0.6rem]">
<Avatar className="ring-2 ring-background">
<AvatarImage
alt="sarah@example.com"
className="grayscale"
src="https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar className="ring-2 ring-background">
<AvatarImage
alt="@maxleiter"
className="grayscale"
src="https://github.com/maxleiter.png"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar className="ring-2 ring-background">
<AvatarImage
alt="@evilrabbit"
className="grayscale"
src="https://github.com/evilrabbit.png"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<button
aria-label="Add collaborator"
className="inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground ring-2 ring-background hover:bg-accent hover:text-accent-foreground"
>
<UserPlusIcon aria-hidden="true" className="size-4" />
</button>
</div>
<div className="space-y-0.5">
<h3 className="font-medium text-sm">No active collaborators</h3>
<p className="text-muted-foreground text-xs">
Invite teammates to start working together.
</p>
</div>
</div>
);
}
With Loading State
Cycles the avatar through image-loading and fallback states to illustrate async image behavior and transitions.
"use client";
import { useEffect, useState } from "react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Spinner } from "@/components/ui/spinner";
export function Pattern() {
const [loading, setLoading] = useState(true);
useEffect(() => {
const interval = setInterval(() => setLoading((prev) => !prev), 2000); // Toggle loading state every 3 seconds
return () => clearInterval(interval);
}, []);
return (
<div className="relative">
<Avatar>
<AvatarImage alt="@shadcn" src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
{loading && (
<div className="absolute inset-0 flex items-center justify-center rounded-full bg-background/60">
<Spinner className="text-primary" />
</div>
)}
</div>
);
}
With Custom Badge
Adds a custom status or role badge positioned at the bottom-right corner of the avatar.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
const CustomBadge = () => {
return (
<svg className="size-4" viewBox="0 0 15 16">
<path
className="fill-blue-500"
d="M14.5425 6.8973L13.5 5.8398C13.4273 5.76858 13.3699 5.68331 13.3312 5.58919C13.2925 5.49507 13.2734 5.39405 13.275 5.2923V3.7923C13.274 3.58681 13.2324 3.38353 13.1527 3.19414C13.0729 3.00476 12.9565 2.833 12.8101 2.68874C12.6638 2.54448 12.4904 2.43055 12.2998 2.35351C12.1093 2.27647 11.9055 2.23783 11.7 2.2398H10.2C10.0982 2.24141 9.99722 2.22228 9.9031 2.1836C9.80898 2.14492 9.72371 2.08749 9.65249 2.0148L8.60249 0.957304C8.30998 0.665106 7.91344 0.500977 7.49999 0.500977C7.08654 0.500977 6.68999 0.665106 6.39749 0.957304L5.33999 1.9998C5.26876 2.07249 5.1835 2.12992 5.08937 2.1686C4.99525 2.20728 4.89424 2.22641 4.79249 2.2248H3.29249C3.08699 2.22578 2.88371 2.26735 2.69432 2.34713C2.50494 2.4269 2.33318 2.54331 2.18892 2.68966C2.04466 2.83602 1.93073 3.00943 1.85369 3.19994C1.77665 3.39046 1.73801 3.59431 1.73999 3.7998V5.2998C1.74159 5.40155 1.72247 5.50256 1.68378 5.59669C1.6451 5.69081 1.58767 5.77608 1.51499 5.8473L0.457487 6.8973C0.165289 7.18981 0.00115967 7.58635 0.00115967 7.9998C0.00115967 8.41325 0.165289 8.80979 0.457487 9.1023L1.49999 10.1598C1.57267 10.231 1.6301 10.3163 1.66878 10.4104C1.70747 10.5045 1.72659 10.6056 1.72499 10.7073V12.2073C1.72597 12.4128 1.76754 12.6161 1.84731 12.8055C1.92709 12.9949 2.04349 13.1666 2.18985 13.3109C2.3362 13.4551 2.50961 13.5691 2.70013 13.6461C2.89064 13.7231 3.0945 13.7618 3.29999 13.7598H4.79999C4.90174 13.7582 5.00275 13.7773 5.09687 13.816C5.191 13.8547 5.27627 13.9121 5.34749 13.9848L6.40499 15.0423C6.69749 15.3345 7.09404 15.4986 7.50749 15.4986C7.92094 15.4986 8.31748 15.3345 8.60999 15.0423L9.65999 13.9998C9.73121 13.9271 9.81647 13.8697 9.9106 13.831C10.0047 13.7923 10.1057 13.7732 10.2075 13.7748H11.7075C12.1212 13.7748 12.518 13.6104 12.8106 13.3179C13.1031 13.0253 13.2675 12.6285 13.2675 12.2148V10.7148C13.2659 10.6131 13.285 10.512 13.3237 10.4179C13.3624 10.3238 13.4198 10.2385 13.4925 10.1673L14.55 9.1098C14.6953 8.96434 14.8104 8.79157 14.8887 8.60146C14.9671 8.41134 15.007 8.20761 15.0063 8.00199C15.0056 7.79638 14.9643 7.59293 14.8847 7.40334C14.8051 7.21376 14.6888 7.04178 14.5425 6.8973Z"
/>
<path
className="fill-white"
d="M10.635 6.6498L6.95249 10.2498C6.90055 10.3024 6.83864 10.3441 6.77038 10.3724C6.70212 10.4007 6.62889 10.4152 6.55499 10.4148C6.48062 10.4138 6.40719 10.398 6.33896 10.3684C6.27073 10.3388 6.20905 10.2959 6.15749 10.2423L4.37999 8.4423C4.32532 8.39026 4.28169 8.32775 4.25169 8.25849C4.22169 8.18923 4.20593 8.11464 4.20536 8.03916C4.20479 7.96369 4.21941 7.88887 4.24836 7.81916C4.27731 7.74946 4.31999 7.68629 4.37387 7.63342C4.42774 7.58056 4.4917 7.53908 4.56194 7.51145C4.63218 7.48382 4.70726 7.47061 4.78271 7.4726C4.85816 7.4746 4.93244 7.49176 5.00112 7.52306C5.0698 7.55436 5.13148 7.59917 5.18249 7.6548L6.56249 9.0573L9.84749 5.8473C9.95296 5.74197 10.0959 5.6828 10.245 5.6828C10.394 5.6828 10.537 5.74197 10.6425 5.8473C10.6953 5.90016 10.737 5.963 10.7653 6.03216C10.7935 6.10132 10.8077 6.17542 10.807 6.25013C10.8063 6.32483 10.7908 6.39865 10.7612 6.46728C10.7317 6.5359 10.6888 6.59795 10.635 6.6498Z"
/>
</svg>
);
};
export function Pattern() {
return (
<div className="flex flex-wrap gap-4">
<Avatar className="relative size-12 overflow-visible">
<AvatarImage
alt="Aron Thompson"
className="rounded-full"
src="https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>AT</AvatarFallback>
<span className="absolute -bottom-0.5 -left-0.5">
<CustomBadge />
</span>
</Avatar>
<Avatar className="relative size-12 overflow-visible">
<AvatarImage
alt="Aron Thompson"
className="rounded-full"
src="https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>AT</AvatarFallback>
<span className="absolute -top-0.5 -left-0.5">
<CustomBadge />
</span>
</Avatar>
<Avatar className="relative size-12 overflow-visible">
<AvatarImage
alt="Aron Thompson"
className="rounded-full"
src="https://images.unsplash.com/photo-1527980965255-d3b416303d12?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>AT</AvatarFallback>
<span className="absolute -right-0.5 -bottom-0.5">
<CustomBadge />
</span>
</Avatar>
<Avatar className="relative size-12 overflow-visible">
<AvatarImage
alt="James Brown"
className="rounded-full"
src="https://images.unsplash.com/photo-1543299750-19d1d6297053?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>JB</AvatarFallback>
<span className="absolute -top-0.5 -right-0.5">
<CustomBadge />
</span>
</Avatar>
</div>
);
}
With Ring Animation
A pulsing ring animates around the avatar to draw attention, useful for active or live streaming indicators.
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
export function Pattern() {
return (
<div className="relative w-fit">
<Avatar className="animate-pulse ring-2 ring-green-500 ring-offset-2 ring-offset-background">
<AvatarImage alt="@shadcn" src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<span className="absolute -right-1 -bottom-1 size-3 rounded-full border-2 border-background bg-green-500" />
</div>
);
}
With Dropdown Menu
Opens a Menu when the avatar is clicked, useful for profile quick-actions or account switchers.
import {
ChevronsUpDownIcon,
LogOutIcon,
PlusIcon,
SettingsIcon,
UserIcon,
} from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/menu";
export function Pattern() {
return (
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
className="h-8 gap-1.5 rounded-full pr-2.5 pl-1"
size="sm"
variant="outline"
>
<Avatar className="size-6 border border-background">
<AvatarImage
alt="Liam Thompson"
src="https://images.unsplash.com/photo-1542595913-85d69b0edbaf?w=96&h=96&dpr=2&q=80"
/>
<AvatarFallback>LT</AvatarFallback>
</Avatar>
<span className="font-medium text-xs">Liam Thompson</span>
<ChevronsUpDownIcon
aria-hidden="true"
className="size-3.5 opacity-60"
/>
</Button>
}
/>
<DropdownMenuContent align="center" className="w-44" sideOffset={8}>
<DropdownMenuGroup>
<DropdownMenuLabel>Management</DropdownMenuLabel>
<DropdownMenuItem>
<UserIcon aria-hidden="true" />
<span>Profile</span>
</DropdownMenuItem>
<DropdownMenuItem>
<SettingsIcon aria-hidden="true" />
<span>Settings</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<UserIcon aria-hidden="true" />
<span>Teams</span>
</DropdownMenuItem>
<DropdownMenuItem>
<PlusIcon aria-hidden="true" />
<span>Invite</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive">
<LogOutIcon aria-hidden="true" />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
Presence Grid
A grid of team member avatars each with a color-coded status dot (green for Online, red for Busy, yellow for Away, grey for Offline) positioned at the bottom-right.
Team · 3 online
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Frame, FramePanel } from "@/components/ui/frame";
type Status = "online" | "busy" | "away" | "offline";
const statusColor: Record<Status, string> = {
away: "bg-yellow-400",
busy: "bg-red-500",
offline: "bg-muted-foreground/40",
online: "bg-emerald-500",
};
const members = [
{
avatar:
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
initials: "AJ",
name: "Alex Johnson",
status: "online" as Status,
},
{
avatar:
"https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
initials: "SC",
name: "Sarah Chen",
status: "busy" as Status,
},
{
avatar:
"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=96&h=96&dpr=2&q=80",
initials: "MR",
name: "Marcus Reed",
status: "online" as Status,
},
{
avatar:
"https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
initials: "LP",
name: "Laura Park",
status: "away" as Status,
},
{
avatar:
"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=96&h=96&dpr=2&q=80",
initials: "TK",
name: "Tom Kim",
status: "offline" as Status,
},
{
avatar: "https://github.com/maxleiter.png",
initials: "ML",
name: "Max Leiter",
status: "online" as Status,
},
];
export function Pattern() {
return (
<div className="mx-auto w-full max-w-sm">
<Frame>
<FramePanel>
<p className="mb-3 font-semibold text-muted-foreground text-xs uppercase tracking-widest">
Team · {members.filter((m) => m.status === "online").length} online
</p>
<div className="grid grid-cols-3 gap-4">
{members.map((m) => (
<div
className="flex flex-col items-center gap-1.5"
key={m.initials}
>
<div className="relative">
<Avatar className="size-10">
<AvatarImage alt={m.name} src={m.avatar} />
<AvatarFallback className="text-xs">
{m.initials}
</AvatarFallback>
</Avatar>
<span
className={`absolute right-0 bottom-0 size-2.5 rounded-full ring-2 ring-background ${statusColor[m.status]}`}
/>
</div>
<span className="max-w-full truncate text-center text-muted-foreground text-xs">
{m.name.split(" ")[0]}
</span>
</div>
))}
</div>
</FramePanel>
</Frame>
</div>
);
}
Comment Thread
A vertical list of pull-request style comments — each row shows a small avatar, the author name, a relative timestamp, the comment body, and a like button with count.
This is a great addition! The implementation looks clean. One suggestion — could we add a `size` variant for compact use cases as well?
Agreed on the size variants. Also, the aria-label is missing on the icon-only button — should be easy to fix before merging.
Good catches, both addressed in the latest commit. Marking as ready for review.
import { HeartIcon, MessageCircleIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Frame, FramePanel } from "@/components/ui/frame";
const comments = [
{
avatar:
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
body: "This is a great addition! The implementation looks clean. One suggestion — could we add a `size` variant for compact use cases as well?",
initials: "AJ",
likes: 4,
name: "Alex Johnson",
time: "2h ago",
},
{
avatar:
"https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
body: "Agreed on the size variants. Also, the aria-label is missing on the icon-only button — should be easy to fix before merging.",
initials: "SC",
likes: 2,
name: "Sarah Chen",
time: "1h ago",
},
{
avatar: "https://github.com/shadcn.png",
body: "Good catches, both addressed in the latest commit. Marking as ready for review.",
initials: "SH",
likes: 7,
name: "shadcn",
time: "45m ago",
},
];
export function Pattern() {
return (
<div className="mx-auto w-full max-w-lg">
<Frame>
<FramePanel className="flex items-center gap-2 px-4 py-2.5!">
<MessageCircleIcon className="size-4 text-muted-foreground" />
<span className="font-medium text-sm">
{comments.length} comments
</span>
</FramePanel>
{comments.map((c, i) => (
<FramePanel
className={i === comments.length - 1 ? "rounded-b-xl" : ""}
key={c.initials}
>
<div className="flex gap-3">
<Avatar className="mt-0.5 size-7 shrink-0">
<AvatarImage alt={c.name} src={c.avatar} />
<AvatarFallback className="text-xs">
{c.initials}
</AvatarFallback>
</Avatar>
<div className="flex flex-1 flex-col gap-1.5">
<div className="flex items-center gap-2">
<span className="font-semibold text-sm">{c.name}</span>
<span className="text-muted-foreground text-xs">
{c.time}
</span>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
{c.body}
</p>
<Button
className="h-auto w-fit gap-1 p-0 text-muted-foreground"
size="xs"
variant="ghost"
>
<HeartIcon className="size-3.5" />
<span className="text-xs">{c.likes}</span>
</Button>
</div>
</div>
</FramePanel>
))}
</Frame>
</div>
);
}
Assignee Picker
A toggleable list of candidates where clicking a row selects or deselects that person as an assignee. Selected rows show a check mark and a tinted background.
Assign to
"use client";
import { CheckIcon } from "lucide-react";
import { useState } from "react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Frame, FramePanel } from "@/components/ui/frame";
const candidates = [
{
avatar:
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
initials: "AJ",
name: "Alex Johnson",
role: "Frontend",
},
{
avatar:
"https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
initials: "SC",
name: "Sarah Chen",
role: "Backend",
},
{
avatar:
"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=96&h=96&dpr=2&q=80",
initials: "MR",
name: "Marcus Reed",
role: "DevOps",
},
{
avatar:
"https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
initials: "LP",
name: "Laura Park",
role: "Design",
},
{
avatar:
"https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=96&h=96&dpr=2&q=80",
initials: "TK",
name: "Tom Kim",
role: "QA",
},
];
export function Pattern() {
const [selected, setSelected] = useState<string[]>(["AJ"]);
const toggle = (initials: string) =>
setSelected((prev) =>
prev.includes(initials)
? prev.filter((i) => i !== initials)
: [...prev, initials],
);
return (
<div className="mx-auto w-full max-w-sm">
<Frame>
<FramePanel>
<p className="mb-3 font-medium text-sm">Assign to</p>
<div className="flex flex-col gap-2">
{candidates.map((c) => {
const isSelected = selected.includes(c.initials);
return (
<button
className={`flex w-full items-center gap-2.5 rounded-lg border px-3 py-2 text-left transition-colors hover:bg-accent ${
isSelected
? "border-primary/30 bg-primary/5 dark:bg-primary/10"
: "bg-transparent"
}`}
key={c.initials}
onClick={() => toggle(c.initials)}
type="button"
>
<Avatar className="size-7">
<AvatarImage alt={c.name} src={c.avatar} />
<AvatarFallback className="text-xs">
{c.initials}
</AvatarFallback>
</Avatar>
<div className="flex flex-1 flex-col">
<span className="font-medium text-sm">{c.name}</span>
<span className="text-muted-foreground text-xs">
{c.role}
</span>
</div>
{isSelected && (
<CheckIcon className="size-4 shrink-0 text-primary" />
)}
</button>
);
})}
</div>
</FramePanel>
</Frame>
</div>
);
}
Contributor Leaderboard
A ranked list of contributors with medal emoji for the top three, followed by their avatar, full name, and a commit count with a git icon.
import { GitCommitHorizontalIcon, TrophyIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Frame, FramePanel } from "@/components/ui/frame";
const leaderboard = [
{
avatar:
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=96&h=96&dpr=2&q=80",
commits: 312,
initials: "AJ",
name: "Alex Johnson",
rank: 1,
},
{
avatar:
"https://images.unsplash.com/photo-1519699047748-de8e457a634e?w=96&h=96&dpr=2&q=80",
commits: 247,
initials: "SC",
name: "Sarah Chen",
rank: 2,
},
{
avatar: "https://github.com/maxleiter.png",
commits: 198,
initials: "ML",
name: "Max Leiter",
rank: 3,
},
{
avatar:
"https://images.unsplash.com/photo-1628157588553-5eeea00af15c?w=96&h=96&dpr=2&q=80",
commits: 154,
initials: "MR",
name: "Marcus Reed",
rank: 4,
},
{
avatar:
"https://images.unsplash.com/photo-1584308972272-9e4e7685e80f?w=96&h=96&dpr=2&q=80",
commits: 89,
initials: "LP",
name: "Laura Park",
rank: 5,
},
];
const rankMedal: Record<number, string> = { 1: "🥇", 2: "🥈", 3: "🥉" };
export function Pattern() {
return (
<div className="mx-auto w-full max-w-sm">
<Frame>
<FramePanel className="flex items-center gap-2 px-4 py-2.5!">
<TrophyIcon className="size-4 text-amber-500" />
<span className="font-semibold text-sm">Contributor Leaderboard</span>
<Badge className="ml-auto" size="sm" variant="secondary">
This month
</Badge>
</FramePanel>
{leaderboard.map((entry) => (
<FramePanel
className="flex items-center gap-3 px-4 py-2.5!"
key={entry.initials}
>
<span className="w-5 shrink-0 text-center text-sm">
{rankMedal[entry.rank] ?? (
<span className="text-muted-foreground">{entry.rank}</span>
)}
</span>
<Avatar className="size-8">
<AvatarImage alt={entry.name} src={entry.avatar} />
<AvatarFallback className="text-xs">
{entry.initials}
</AvatarFallback>
</Avatar>
<span className="flex-1 font-medium text-sm">{entry.name}</span>
<span className="flex items-center gap-1 text-muted-foreground text-xs">
<GitCommitHorizontalIcon className="size-3.5" />
{entry.commits}
</span>
</FramePanel>
))}
</Frame>
</div>
);
}
Profile Card
A centered profile layout with a large avatar, name, username, bio, location, website link, a stats row (Repos / Followers / Following), and Follow + Message action buttons.
Building open-source tools for developers. TypeScript enthusiast and design systems advocate.
import { LinkIcon, MapPinIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Frame, FramePanel } from "@/components/ui/frame";
import { Separator } from "@/components/ui/separator";
const stats = [
{ label: "Repos", value: "142" },
{ label: "Followers", value: "8.4k" },
{ label: "Following", value: "91" },
];
export function Pattern() {
return (
<div className="mx-auto w-full max-w-xs">
<Frame>
<FramePanel>
<div className="flex flex-col items-center gap-3 text-center">
<Avatar className="size-20 ring-2 ring-background ring-offset-2 ring-offset-muted">
<AvatarImage
alt="Jordan Blake"
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=128&h=128&dpr=2&q=80"
/>
<AvatarFallback className="text-xl">JB</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
<div className="flex items-center justify-center gap-1.5">
<span className="font-semibold text-base">Jordan Blake</span>
<Badge size="sm" variant="success">
Pro
</Badge>
</div>
<span className="text-muted-foreground text-sm">
@jordanblake
</span>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
Building open-source tools for developers. TypeScript enthusiast
and design systems advocate.
</p>
<div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-muted-foreground text-xs">
<span className="flex items-center gap-1">
<MapPinIcon className="size-3" />
San Francisco, CA
</span>
<span className="flex items-center gap-1">
<LinkIcon className="size-3" />
jordanblake.dev
</span>
</div>
</div>
<Separator className="my-3" />
<div className="grid grid-cols-3 divide-x text-center">
{stats.map((s) => (
<div className="flex flex-col py-1" key={s.label}>
<span className="font-semibold text-sm">{s.value}</span>
<span className="text-muted-foreground text-xs">{s.label}</span>
</div>
))}
</div>
<div className="mt-3 flex gap-2">
<Button className="flex-1" size="sm">
Follow
</Button>
<Button className="flex-1" size="sm" variant="outline">
Message
</Button>
</div>
</FramePanel>
</Frame>
</div>
);
}
On This Page

