With Name Avatar
Original · freeavatar with name and role
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/av-with-name.json"use client";
import { useCallback, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Presence = "online" | "away" | "offline";
interface Member {
id: string;
name: string;
role: string;
email: string;
location: string;
bio: string;
from: string;
to: string;
presence: Presence;
}
const MEMBERS: Member[] = [
{
id: "amara-okafor",
name: "Amara Okafor",
role: "Principal Product Designer",
email: "amara.okafor@atlasand.co",
location: "Lagos, Nigeria",
bio: "Leads product design across the mobile apps and owns the end-to-end checkout redesign shipping this quarter.",
from: "from-indigo-500",
to: "to-violet-500",
presence: "online",
},
{
id: "devin-cross",
name: "Devin Cross",
role: "Staff Frontend Engineer",
email: "devin.cross@atlasand.co",
location: "Austin, USA",
bio: "Builds the component library and keeps the render budget honest on every release, down to the last millisecond.",
from: "from-sky-500",
to: "to-indigo-500",
presence: "online",
},
{
id: "priya-nair",
name: "Priya Nair",
role: "Design Systems Lead",
email: "priya.nair@atlasand.co",
location: "Bengaluru, India",
bio: "Maintains the token pipeline and the accessibility standards the whole team ships against, one audit at a time.",
from: "from-violet-500",
to: "to-rose-500",
presence: "away",
},
{
id: "lena-fischer",
name: "Lena Fischer",
role: "Senior UX Researcher",
email: "lena.fischer@atlasand.co",
location: "Berlin, Germany",
bio: "Runs the research practice and turns raw interviews into decisions the product team can actually act on.",
from: "from-emerald-500",
to: "to-sky-500",
presence: "online",
},
{
id: "marco-bianchi",
name: "Marco Bianchi",
role: "Motion Designer",
email: "marco.bianchi@atlasand.co",
location: "Milan, Italy",
bio: "Prototypes interaction and motion, from the smallest micro-transition to a full onboarding sequence.",
from: "from-amber-500",
to: "to-rose-500",
presence: "offline",
},
{
id: "theo-almeida",
name: "Theo Almeida",
role: "Engineering Manager",
email: "theo.almeida@atlasand.co",
location: "Lisbon, Portugal",
bio: "Coaches the platform team and unblocks the roadmap one thoughtful code review at a time.",
from: "from-rose-500",
to: "to-amber-500",
presence: "away",
},
];
const PRESENCE: Record<
Presence,
{ label: string; dot: string; ring: string; text: string }
> = {
online: {
label: "Online",
dot: "bg-emerald-500",
ring: "bg-emerald-400",
text: "text-emerald-600 dark:text-emerald-400",
},
away: {
label: "Away",
dot: "bg-amber-500",
ring: "bg-amber-400",
text: "text-amber-600 dark:text-amber-400",
},
offline: {
label: "Offline",
dot: "bg-slate-400 dark:bg-slate-600",
ring: "bg-slate-300",
text: "text-slate-500 dark:text-slate-400",
},
};
function initials(name: string): string {
const parts = name.trim().split(/\s+/);
const first = parts[0]?.[0] ?? "";
const last = parts.length > 1 ? parts[parts.length - 1][0] : "";
return (first + last).toUpperCase();
}
function Avatar({
member,
size,
}: {
member: Member;
size: "sm" | "lg";
}) {
const dims =
size === "lg" ? "h-16 w-16 text-xl" : "h-11 w-11 text-sm";
const dotSize = size === "lg" ? "h-4 w-4" : "h-3 w-3";
const p = PRESENCE[member.presence];
return (
<span className="relative inline-flex shrink-0">
<span
aria-hidden="true"
className={`inline-flex items-center justify-center rounded-full bg-gradient-to-br font-semibold tracking-tight text-white shadow-sm ${member.from} ${member.to} ${dims}`}
>
{initials(member.name)}
</span>
<span
className={`absolute bottom-0 right-0 flex items-center justify-center rounded-full ring-2 ring-white dark:ring-slate-900 ${dotSize}`}
>
{member.presence === "online" && (
<span
aria-hidden="true"
className={`avwn-ping absolute inline-flex h-full w-full rounded-full ${p.ring}`}
/>
)}
<span className={`relative inline-flex rounded-full ${dotSize} ${p.dot}`} />
</span>
</span>
);
}
export default function AvWithName() {
const reduce = useReducedMotion();
const [onlineOnly, setOnlineOnly] = useState(false);
const [selectedId, setSelectedId] = useState<string>(MEMBERS[0].id);
const [activeIndex, setActiveIndex] = useState(0);
const [copied, setCopied] = useState(false);
const optionRefs = useRef<(HTMLLIElement | null)[]>([]);
const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const roster = useMemo(
() => (onlineOnly ? MEMBERS.filter((m) => m.presence === "online") : MEMBERS),
[onlineOnly]
);
const onlineCount = useMemo(
() => MEMBERS.filter((m) => m.presence === "online").length,
[]
);
const selected =
roster.find((m) => m.id === selectedId) ?? roster[0] ?? MEMBERS[0];
const safeActive = Math.min(activeIndex, Math.max(roster.length - 1, 0));
const selectAt = useCallback(
(index: number) => {
const member = roster[index];
if (!member) return;
setActiveIndex(index);
setSelectedId(member.id);
},
[roster]
);
const focusAt = useCallback((index: number) => {
optionRefs.current[index]?.focus();
}, []);
const onKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLUListElement>) => {
const count = roster.length;
if (count === 0) return;
let next = safeActive;
switch (event.key) {
case "ArrowDown":
next = (safeActive + 1) % count;
break;
case "ArrowUp":
next = (safeActive - 1 + count) % count;
break;
case "Home":
next = 0;
break;
case "End":
next = count - 1;
break;
case "Enter":
case " ":
event.preventDefault();
selectAt(safeActive);
return;
default:
return;
}
event.preventDefault();
setActiveIndex(next);
focusAt(next);
},
[roster.length, safeActive, selectAt, focusAt]
);
const toggleOnline = useCallback(() => {
setOnlineOnly((prev) => {
const nextRoster = !prev
? MEMBERS.filter((m) => m.presence === "online")
: MEMBERS;
const stillThere = nextRoster.some((m) => m.id === selectedId);
if (!stillThere && nextRoster[0]) setSelectedId(nextRoster[0].id);
setActiveIndex(0);
return !prev;
});
}, [selectedId]);
const copyEmail = useCallback(async () => {
try {
if (typeof navigator !== "undefined" && navigator.clipboard) {
await navigator.clipboard.writeText(selected.email);
}
setCopied(true);
if (copyTimer.current) clearTimeout(copyTimer.current);
copyTimer.current = setTimeout(() => setCopied(false), 2000);
} catch {
setCopied(false);
}
}, [selected.email]);
const dur = reduce ? 0 : 0.28;
return (
<section className="relative w-full bg-slate-50 px-5 py-16 text-slate-900 sm:px-8 sm:py-24 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes avwn-ping {
0% { transform: scale(1); opacity: 0.6; }
75%, 100% { transform: scale(2.3); opacity: 0; }
}
.avwn-ping { animation: avwn-ping 2s cubic-bezier(0, 0, 0.2, 1) infinite; }
@media (prefers-reduced-motion: reduce) {
.avwn-ping { animation: none; }
}
`}</style>
<div className="mx-auto max-w-5xl">
<header className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
<div className="max-w-xl">
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-indigo-600 dark:text-indigo-400">
Atlas & Co.
</p>
<h2 className="mt-3 text-3xl font-semibold tracking-tight sm:text-4xl">
Meet the crew
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
Six people who ship the product — designers, engineers, and
researchers working across four time zones.{" "}
<span className="font-medium text-slate-900 dark:text-slate-200">
{onlineCount} online now.
</span>
</p>
</div>
<button
type="button"
role="switch"
aria-checked={onlineOnly}
onClick={toggleOnline}
className="group inline-flex items-center gap-3 self-start rounded-full border border-slate-200 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:border-slate-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-200 dark:hover:border-slate-700 dark:focus-visible:ring-offset-slate-950"
>
<span>Online only</span>
<span
aria-hidden="true"
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${
onlineOnly
? "bg-emerald-500"
: "bg-slate-300 dark:bg-slate-700"
}`}
>
<span
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow-sm transition-transform ${
onlineOnly ? "translate-x-[22px]" : "translate-x-0.5"
}`}
/>
</span>
</button>
</header>
<div className="mt-10 grid gap-6 lg:grid-cols-[minmax(0,1fr)_360px]">
{/* Roster */}
<div className="rounded-2xl border border-slate-200 bg-white p-2 shadow-sm dark:border-slate-800 dark:bg-slate-900">
<ul
role="listbox"
aria-label="Team members"
onKeyDown={onKeyDown}
className="flex flex-col gap-1"
>
{roster.map((member, index) => {
const isSelected = member.id === selected.id;
const p = PRESENCE[member.presence];
return (
<li
key={member.id}
ref={(el) => {
optionRefs.current[index] = el;
}}
id={`avwn-opt-${member.id}`}
role="option"
aria-selected={isSelected}
tabIndex={index === safeActive ? 0 : -1}
onClick={() => selectAt(index)}
className={`flex cursor-pointer items-center gap-4 rounded-xl border px-3 py-3 outline-none transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900 ${
isSelected
? "border-indigo-200 bg-indigo-50 dark:border-indigo-500/40 dark:bg-indigo-500/10"
: "border-transparent hover:bg-slate-50 dark:hover:bg-slate-800/60"
}`}
>
<Avatar member={member} size="sm" />
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-semibold text-slate-900 dark:text-slate-100">
{member.name}
</span>
<span className="block truncate text-sm text-slate-500 dark:text-slate-400">
{member.role}
</span>
</span>
<span
className={`hidden shrink-0 items-center gap-1.5 text-xs font-medium sm:inline-flex ${p.text}`}
>
<span
aria-hidden="true"
className={`h-1.5 w-1.5 rounded-full ${p.dot}`}
/>
{p.label}
</span>
</li>
);
})}
</ul>
{roster.length === 0 && (
<p className="px-4 py-10 text-center text-sm text-slate-500 dark:text-slate-400">
No one is online right now.
</p>
)}
</div>
{/* Detail */}
<aside
aria-live="polite"
className="relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900"
>
<AnimatePresence mode="wait">
<motion.div
key={selected.id}
initial={reduce ? false : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8 }}
transition={{ duration: dur, ease: [0.22, 1, 0.36, 1] }}
>
<div className="flex items-center gap-4">
<Avatar member={selected} size="lg" />
<div className="min-w-0">
<h3 className="truncate text-lg font-semibold tracking-tight text-slate-900 dark:text-slate-100">
{selected.name}
</h3>
<p className="truncate text-sm text-slate-500 dark:text-slate-400">
{selected.role}
</p>
</div>
</div>
<dl className="mt-6 space-y-3 text-sm">
<div className="flex items-center gap-2">
<dt className="sr-only">Status</dt>
<dd
className={`inline-flex items-center gap-1.5 font-medium ${PRESENCE[selected.presence].text}`}
>
<span
aria-hidden="true"
className={`h-2 w-2 rounded-full ${PRESENCE[selected.presence].dot}`}
/>
{PRESENCE[selected.presence].label}
</dd>
</div>
<div className="flex items-start gap-2 text-slate-600 dark:text-slate-400">
<dt className="sr-only">Location</dt>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
className="mt-0.5 h-4 w-4 shrink-0"
>
<path
d="M12 21s-6-5.2-6-10a6 6 0 1 1 12 0c0 4.8-6 10-6 10Z"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle
cx="12"
cy="11"
r="2.2"
stroke="currentColor"
strokeWidth="1.6"
/>
</svg>
<dd>{selected.location}</dd>
</div>
</dl>
<p className="mt-5 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
{selected.bio}
</p>
<div className="mt-6 flex flex-wrap gap-2">
<a
href={`mailto:${selected.email}`}
className="inline-flex items-center gap-2 rounded-lg bg-slate-900 px-3.5 py-2 text-sm font-medium text-white shadow-sm transition-colors hover:bg-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-900"
>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
className="h-4 w-4"
>
<rect
x="3"
y="5"
width="18"
height="14"
rx="2"
stroke="currentColor"
strokeWidth="1.6"
/>
<path
d="m4 7 8 6 8-6"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Message
</a>
<button
type="button"
onClick={copyEmail}
className="inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3.5 py-2 text-sm font-medium text-slate-700 shadow-sm transition-colors hover:border-slate-300 hover:bg-slate-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
{copied ? (
<>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
className="h-4 w-4 text-emerald-600 dark:text-emerald-400"
>
<path
d="m5 12 4.5 4.5L19 7"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Copied
</>
) : (
<>
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
className="h-4 w-4"
>
<rect
x="9"
y="9"
width="11"
height="11"
rx="2"
stroke="currentColor"
strokeWidth="1.6"
/>
<path
d="M5 15V5a2 2 0 0 1 2-2h10"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
Copy email
</>
)}
</button>
</div>
<span className="sr-only" role="status">
{copied ? `${selected.email} copied to clipboard` : ""}
</span>
</motion.div>
</AnimatePresence>
</aside>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Avatar
Originalavatars in sizes with fallback

Group Avatar
Originaloverlapping avatar group

Status Avatar
Originalavatars with online status dot

Ring Avatar
Originalavatars with gradient ring

Initials Avatar
Originalinitials avatars with colours

Shapes Avatar
Originalavatars in circle/square/squircle

Upload Avatar
Originalavatar with upload/edit overlay

Stacked Count Avatar
Originalavatar stack with +N count

Squircle Avatar
Originalsquircle avatars with badge

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

