Initials Avatar
Original · freeinitials avatars with colours
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-initials.json"use client";
import { useId, useRef, useState } from "react";
import type {
ChangeEvent,
FormEvent,
KeyboardEvent as ReactKeyboardEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Size = "sm" | "md" | "lg" | "xl";
type Shape = "circle" | "square";
type Variant = "soft" | "solid";
type StatusKey = "online" | "away" | "busy" | "offline";
type Teammate = {
id: string;
name: string;
role: string;
status: StatusKey;
};
type Palette = {
name: string;
soft: string;
solid: string;
};
const PALETTES: ReadonlyArray<Palette> = [
{
name: "indigo",
soft: "bg-indigo-100 text-indigo-700 dark:bg-indigo-400/15 dark:text-indigo-300",
solid: "bg-indigo-600 text-white",
},
{
name: "violet",
soft: "bg-violet-100 text-violet-700 dark:bg-violet-400/15 dark:text-violet-300",
solid: "bg-violet-600 text-white",
},
{
name: "emerald",
soft: "bg-emerald-100 text-emerald-700 dark:bg-emerald-400/15 dark:text-emerald-300",
solid: "bg-emerald-600 text-white",
},
{
name: "rose",
soft: "bg-rose-100 text-rose-700 dark:bg-rose-400/15 dark:text-rose-300",
solid: "bg-rose-600 text-white",
},
{
name: "amber",
soft: "bg-amber-100 text-amber-800 dark:bg-amber-400/15 dark:text-amber-300",
solid: "bg-amber-600 text-white",
},
{
name: "sky",
soft: "bg-sky-100 text-sky-700 dark:bg-sky-400/15 dark:text-sky-300",
solid: "bg-sky-600 text-white",
},
{
name: "slate",
soft: "bg-slate-200 text-slate-700 dark:bg-slate-400/15 dark:text-slate-300",
solid: "bg-slate-700 text-white",
},
{
name: "zinc",
soft: "bg-zinc-200 text-zinc-700 dark:bg-zinc-400/15 dark:text-zinc-300",
solid: "bg-zinc-700 text-white",
},
];
const SIZE: Record<Size, { box: string; text: string; dot: string }> = {
sm: { box: "h-9 w-9", text: "text-xs", dot: "h-2.5 w-2.5" },
md: { box: "h-12 w-12", text: "text-sm", dot: "h-3 w-3" },
lg: { box: "h-16 w-16", text: "text-lg", dot: "h-3.5 w-3.5" },
xl: { box: "h-24 w-24", text: "text-3xl", dot: "h-5 w-5" },
};
const STATUS: Record<StatusKey, { label: string; dot: string; live: boolean }> = {
online: { label: "Online", dot: "bg-emerald-500", live: true },
away: { label: "Away", dot: "bg-amber-500", live: false },
busy: { label: "In a meeting", dot: "bg-rose-500", live: false },
offline: { label: "Offline", dot: "bg-slate-400 dark:bg-slate-500", live: false },
};
const RING = "ring-2 ring-white dark:ring-slate-900";
const FOCUS =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";
function hashString(str: string): number {
let h = 0;
for (let i = 0; i < str.length; i += 1) {
h = (h << 5) - h + str.charCodeAt(i);
h |= 0;
}
return Math.abs(h);
}
function paletteFor(name: string): Palette {
const key = name.trim().toLowerCase();
return PALETTES[hashString(key) % PALETTES.length];
}
function getInitials(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "?";
if (parts.length === 1) {
const w = parts[0];
return (w.length > 1 ? w.slice(0, 2) : w).toUpperCase();
}
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
const STYLE_OPTIONS: ReadonlyArray<{ value: Variant; label: string }> = [
{ value: "soft", label: "Soft" },
{ value: "solid", label: "Solid" },
];
const SHAPE_OPTIONS: ReadonlyArray<{ value: Shape; label: string }> = [
{ value: "circle", label: "Circle" },
{ value: "square", label: "Rounded" },
];
const SIZE_OPTIONS: ReadonlyArray<{ value: Size; label: string }> = [
{ value: "sm", label: "S" },
{ value: "md", label: "M" },
{ value: "lg", label: "L" },
{ value: "xl", label: "XL" },
];
const SEED_TEAM: ReadonlyArray<Teammate> = [
{ id: "seed-1", name: "Amara Okafor", role: "Product Designer", status: "online" },
{ id: "seed-2", name: "Liang Wei", role: "Staff Engineer", status: "busy" },
{ id: "seed-3", name: "Sofia Marchetti", role: "Data Scientist", status: "away" },
{ id: "seed-4", name: "Ravi Deshmukh", role: "Engineering Manager", status: "online" },
{ id: "seed-5", name: "Noor Al-Rashid", role: "UX Researcher", status: "offline" },
{ id: "seed-6", name: "Diego Fuentes", role: "Frontend Engineer", status: "online" },
];
function Avatar({
name,
size,
shape,
variant,
status,
showStatus,
decorative = false,
sheen = false,
reduce = false,
}: {
name: string;
size: Size;
shape: Shape;
variant: Variant;
status: StatusKey;
showStatus: boolean;
decorative?: boolean;
sheen?: boolean;
reduce?: boolean;
}) {
const palette = paletteFor(name);
const colorCls = variant === "soft" ? palette.soft : palette.solid;
const shapeCls = shape === "circle" ? "rounded-full" : "rounded-2xl";
const dims = SIZE[size];
const st = STATUS[status];
return (
<span className={`relative inline-flex shrink-0 ${dims.box}`}>
<span
className={`relative flex h-full w-full items-center justify-center overflow-hidden font-semibold tracking-tight select-none ${shapeCls} ${colorCls} ${dims.text}`}
role={decorative ? undefined : "img"}
aria-hidden={decorative ? true : undefined}
aria-label={decorative ? undefined : name.trim() || "Unnamed teammate"}
title={decorative ? undefined : name.trim() || undefined}
>
{getInitials(name)}
{sheen && !reduce ? (
<span className="avin-shine pointer-events-none absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent" />
) : null}
</span>
{showStatus ? (
<span className="absolute bottom-0 right-0 flex">
{st.live && !reduce ? (
<span
className={`avin-ping absolute inset-0 rounded-full bg-emerald-400 ${dims.dot}`}
aria-hidden
/>
) : null}
<span
className={`relative block rounded-full ${dims.dot} ${st.dot} ${RING}`}
aria-hidden
/>
</span>
) : null}
</span>
);
}
function Segmented<T extends string>({
label,
value,
options,
onChange,
}: {
label: string;
value: T;
options: ReadonlyArray<{ value: T; label: string }>;
onChange: (value: T) => void;
}) {
const refs = useRef<Array<HTMLButtonElement | null>>([]);
const current = options.findIndex((o) => o.value === value);
const selectAt = (index: number) => {
const next = (index + options.length) % options.length;
onChange(options[next].value);
refs.current[next]?.focus();
};
const onKeyDown = (e: ReactKeyboardEvent<HTMLButtonElement>) => {
switch (e.key) {
case "ArrowRight":
case "ArrowDown":
e.preventDefault();
selectAt(current + 1);
break;
case "ArrowLeft":
case "ArrowUp":
e.preventDefault();
selectAt(current - 1);
break;
case "Home":
e.preventDefault();
selectAt(0);
break;
case "End":
e.preventDefault();
selectAt(options.length - 1);
break;
default:
break;
}
};
return (
<div>
<span className="mb-2 block text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
{label}
</span>
<div
role="radiogroup"
aria-label={label}
className="inline-flex gap-1 rounded-xl bg-slate-100 p-1 dark:bg-slate-800"
>
{options.map((o, i) => {
const active = o.value === value;
return (
<button
key={o.value}
type="button"
role="radio"
aria-checked={active}
tabIndex={active ? 0 : -1}
ref={(el) => {
refs.current[i] = el;
}}
onClick={() => onChange(o.value)}
onKeyDown={onKeyDown}
className={`min-w-[3rem] rounded-lg px-3 py-1.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-indigo-400 ${
active
? "bg-white text-slate-900 shadow-sm dark:bg-slate-950 dark:text-white"
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-200"
}`}
>
{o.label}
</button>
);
})}
</div>
</div>
);
}
export default function AvInitials() {
const reduce = !!useReducedMotion();
const headingId = useId();
const nameId = useId();
const statusId = useId();
const [variant, setVariant] = useState<Variant>("soft");
const [shape, setShape] = useState<Shape>("circle");
const [size, setSize] = useState<Size>("lg");
const [showStatus, setShowStatus] = useState(true);
const [team, setTeam] = useState<Teammate[]>(() => SEED_TEAM.slice());
const [draftName, setDraftName] = useState("Priya Nair");
const [draftStatus, setDraftStatus] = useState<StatusKey>("online");
const idRef = useRef(0);
const trimmed = draftName.trim();
const previewPalette = paletteFor(trimmed || "?");
const handleAdd = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const name = draftName.trim();
if (!name) return;
idRef.current += 1;
setTeam((prev) => [
{ id: `added-${idRef.current}`, name, role: "New teammate", status: draftStatus },
...prev,
]);
setDraftName("");
};
const removeMember = (id: string) => {
setTeam((prev) => prev.filter((m) => m.id !== id));
};
const stackVisible = team.slice(0, 6);
const overflow = team.length - stackVisible.length;
const itemInitial = reduce ? { opacity: 0 } : { opacity: 0, y: 10, scale: 0.96 };
const itemAnimate = { opacity: 1, y: 0, scale: 1 };
const itemExit = reduce
? { opacity: 0, transition: { duration: 0.12 } }
: { opacity: 0, scale: 0.9, transition: { duration: 0.16 } };
const itemTransition = reduce
? { duration: 0 }
: { type: "spring" as const, stiffness: 420, damping: 32 };
return (
<section
aria-labelledby={headingId}
className="relative w-full overflow-hidden bg-slate-50 px-5 py-20 sm:px-8 dark:bg-slate-950"
>
<style>{`
@keyframes avin-ping {
0% { transform: scale(1); opacity: 0.55; }
70%, 100% { transform: scale(2); opacity: 0; }
}
@keyframes avin-shine {
0% { transform: translateX(-130%) skewX(-12deg); opacity: 0; }
28% { opacity: 0.6; }
60%, 100% { transform: translateX(130%) skewX(-12deg); opacity: 0; }
}
.avin-ping { animation: avin-ping 2.2s cubic-bezier(0, 0, 0.2, 1) infinite; }
.avin-shine { animation: avin-shine 3.4s ease-in-out 0.5s infinite; }
@media (prefers-reduced-motion: reduce) {
.avin-ping, .avin-shine { animation: none !important; }
}
`}</style>
<div
aria-hidden
className="pointer-events-none absolute -right-24 -top-24 h-72 w-72 rounded-full bg-indigo-300/25 blur-3xl dark:bg-indigo-500/10"
/>
<div
aria-hidden
className="pointer-events-none absolute -bottom-24 -left-24 h-72 w-72 rounded-full bg-violet-300/25 blur-3xl dark:bg-violet-500/10"
/>
<div className="relative mx-auto max-w-5xl">
<header className="max-w-2xl">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium uppercase tracking-wider text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
Avatars
</span>
<h2
id={headingId}
className="mt-4 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white"
>
Initials, coloured by name
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-300">
Every teammate gets a consistent colour derived from their name — the same
person always lands on the same hue, no photos required. Tune the style, shape
and size, then add someone to watch it generated instantly.
</p>
</header>
<div className="mt-10 grid gap-5 lg:grid-cols-5">
<div className="rounded-2xl border border-slate-200 bg-white p-6 lg:col-span-3 dark:border-slate-800 dark:bg-slate-900">
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">
Display settings
</h3>
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
Applies to every avatar on this page.
</p>
<div className="mt-5 flex flex-wrap gap-x-8 gap-y-5">
<Segmented label="Style" value={variant} options={STYLE_OPTIONS} onChange={setVariant} />
<Segmented label="Shape" value={shape} options={SHAPE_OPTIONS} onChange={setShape} />
<Segmented label="Size" value={size} options={SIZE_OPTIONS} onChange={setSize} />
</div>
<div className="mt-6 flex items-center justify-between gap-4 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-950/60">
<span className="flex flex-col">
<span className="text-sm font-medium text-slate-900 dark:text-white">
Presence indicators
</span>
<span className="text-xs text-slate-500 dark:text-slate-400">
Show the live status dot on each avatar.
</span>
</span>
<button
type="button"
role="switch"
aria-checked={showStatus}
aria-label="Toggle presence indicators"
onClick={() => setShowStatus((v) => !v)}
className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors ${FOCUS} ${
showStatus ? "bg-indigo-600" : "bg-slate-300 dark:bg-slate-700"
}`}
>
<span
className={`inline-block h-5 w-5 transform rounded-full bg-white shadow-sm transition-transform ${
showStatus ? "translate-x-5" : "translate-x-0.5"
}`}
/>
</button>
</div>
</div>
<div className="rounded-2xl border border-slate-200 bg-white p-6 lg:col-span-2 dark:border-slate-800 dark:bg-slate-900">
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">
Live preview
</h3>
<div className="mt-5 flex items-center gap-4">
<Avatar
name={trimmed || "?"}
size="xl"
shape={shape}
variant={variant}
status={draftStatus}
showStatus={showStatus}
decorative
sheen
reduce={reduce}
/>
<div className="min-w-0">
{trimmed ? (
<>
<p className="truncate text-sm font-medium text-slate-900 dark:text-white">
{trimmed}
</p>
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
Initials{" "}
<span className="font-semibold text-slate-700 dark:text-slate-200">
{getInitials(trimmed)}
</span>{" "}
· colour{" "}
<span className="font-semibold capitalize text-slate-700 dark:text-slate-200">
{previewPalette.name}
</span>
</p>
</>
) : (
<p className="text-sm text-slate-500 dark:text-slate-400">
Type a name to preview its avatar.
</p>
)}
</div>
</div>
<form onSubmit={handleAdd} className="mt-6 space-y-4">
<div>
<label
htmlFor={nameId}
className="mb-1.5 block text-xs font-medium text-slate-600 dark:text-slate-300"
>
Full name
</label>
<input
id={nameId}
type="text"
value={draftName}
onChange={(e: ChangeEvent<HTMLInputElement>) => setDraftName(e.target.value)}
placeholder="e.g. Grace Hopper"
autoComplete="off"
className={`w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-950 dark:text-white dark:placeholder:text-slate-500 ${FOCUS}`}
/>
</div>
<div>
<label
htmlFor={statusId}
className="mb-1.5 block text-xs font-medium text-slate-600 dark:text-slate-300"
>
Presence
</label>
<select
id={statusId}
value={draftStatus}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
setDraftStatus(e.target.value as StatusKey)
}
className={`w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 dark:border-slate-700 dark:bg-slate-950 dark:text-white ${FOCUS}`}
>
<option value="online">Online</option>
<option value="away">Away</option>
<option value="busy">In a meeting</option>
<option value="offline">Offline</option>
</select>
</div>
<button
type="submit"
disabled={!trimmed}
className={`inline-flex w-full items-center justify-center gap-2 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-40 ${FOCUS}`}
>
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden
className="h-4 w-4"
>
<path
d="M10 4.5v11M4.5 10h11"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
/>
</svg>
Add to team
</button>
</form>
</div>
</div>
<div className="mt-10">
<div className="flex items-baseline justify-between">
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">
Your team
</h3>
<span className="text-xs tabular-nums text-slate-500 dark:text-slate-400">
{team.length} {team.length === 1 ? "person" : "people"}
</span>
</div>
<ul className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2">
<AnimatePresence initial={false} mode="popLayout">
{team.map((m) => {
const st = STATUS[m.status];
return (
<motion.li
key={m.id}
layout={!reduce}
initial={itemInitial}
animate={itemAnimate}
exit={itemExit}
transition={itemTransition}
className="group flex items-center gap-3 rounded-xl border border-slate-200 bg-white p-3 dark:border-slate-800 dark:bg-slate-900"
>
<Avatar
name={m.name}
size={size === "xl" ? "lg" : size}
shape={shape}
variant={variant}
status={m.status}
showStatus={showStatus}
decorative
reduce={reduce}
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-slate-900 dark:text-white">
{m.name}
</p>
<p className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
<span className={`h-1.5 w-1.5 rounded-full ${st.dot}`} aria-hidden />
<span className="truncate">
{m.role} · {st.label}
</span>
</p>
</div>
<button
type="button"
onClick={() => removeMember(m.id)}
aria-label={`Remove ${m.name}`}
className={`inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 ${FOCUS}`}
>
<svg viewBox="0 0 20 20" fill="none" aria-hidden className="h-4 w-4">
<path
d="M6 6l8 8M14 6l-8 8"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
/>
</svg>
</button>
</motion.li>
);
})}
</AnimatePresence>
</ul>
{team.length === 0 ? (
<p className="mt-4 rounded-xl border border-dashed border-slate-300 bg-white/50 px-4 py-8 text-center text-sm text-slate-500 dark:border-slate-700 dark:bg-slate-900/40 dark:text-slate-400">
No teammates yet — add someone above to generate their avatar.
</p>
) : null}
</div>
{team.length > 0 ? (
<div className="mt-8 flex flex-wrap items-center gap-4 rounded-2xl border border-slate-200 bg-white p-5 dark:border-slate-800 dark:bg-slate-900">
<div className="flex items-center">
<div className="flex -space-x-3">
{stackVisible.map((m) => (
<span key={m.id} className="relative">
<span className={`inline-flex rounded-full ${RING}`}>
<Avatar
name={m.name}
size="md"
shape="circle"
variant={variant}
status={m.status}
showStatus={false}
reduce={reduce}
/>
</span>
</span>
))}
{overflow > 0 ? (
<span
className={`relative inline-flex h-12 w-12 items-center justify-center rounded-full bg-slate-200 text-sm font-semibold text-slate-600 dark:bg-slate-800 dark:text-slate-300 ${RING}`}
aria-hidden
>
+{overflow}
</span>
) : null}
</div>
</div>
<div className="min-w-0">
<p className="text-sm font-semibold text-slate-900 dark:text-white">
Working on Project Aurora
</p>
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
{team.length} contributor{team.length === 1 ? "" : "s"} in the shared workspace
</p>
</div>
</div>
) : null}
</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

Shapes Avatar
Originalavatars in circle/square/squircle

Upload Avatar
Originalavatar with upload/edit overlay

Stacked Count Avatar
Originalavatar stack with +N count

With Name Avatar
Originalavatar with name and role

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.

