Avatar
Menschen als Bild oder Initialen, mit Status, im Stapel und mit Namen.
Eine von 5 Components in Data Display
Variants
Details
Was Avatar mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Data Display
- Bereich
- Elements
- Variants
- 4
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/avatar.tsx
- Bringt mit
- utilskit
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 6 eigene Texte
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle AvatarProps aus components/ui/avatar.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
namePflichtstringFull name, used for initials, the alt text and the tone
srcoptionalstringImage URL. Falls back to initials when it fails to load.
altoptionalstringAlt text, defaults to the name. Pass "" when the name is written next to the avatar.
imageoptionalReact.ReactNodeAny drawn or custom image, shown instead of src
sizeoptionalAvatarSizexssmmdlgxl2xl
shapeoptional'circle' | 'square'statusoptionalAvatarStatusonlineawaybusyoffline
toneoptionalAvatarToneautoaccentneutralsolid
Color of the initials. auto picks a calm tone from the name.
ringoptionalbooleanRing in the page background, for avatars on images or in stacks
Eingebaute Texte
Diese Texte schreibt Avatar selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 6 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| online | Englisch online | Deutsch online |
| away | Englisch away | Deutsch abwesend |
| busy | Englisch busy | Deutsch beschäftigt |
| offline | Englisch offline | Deutsch offline |
| more | Englisch more | Deutsch weitere |
| people | Englisch people | Deutsch Personen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle group.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 3 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 248 Zeilen
'use client';
import { useRef, useState } from 'react';
import { cn, useInView, useTexts } from '@/lib/kit';
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
export type AvatarStatus = 'online' | 'away' | 'busy' | 'offline';
export type AvatarTone = 'auto' | 'accent' | 'neutral' | 'solid';
const SIZE: Record<AvatarSize, { box: string; text: string; dot: string; px: number }> = {
xs: { box: 'size-6', text: 'text-[9px]', dot: 'size-2', px: 24 },
sm: { box: 'size-8', text: 'text-[11px]', dot: 'size-2.5', px: 32 },
md: { box: 'size-10', text: 'text-[13px]', dot: 'size-3', px: 40 },
lg: { box: 'size-12', text: 'text-[15px]', dot: 'size-3', px: 48 },
xl: { box: 'size-16', text: 'text-[19px]', dot: 'size-3.5', px: 64 },
'2xl': { box: 'size-20', text: 'text-[23px]', dot: 'size-4', px: 80 },
};
const TONES = [
'bg-[color-mix(in_oklab,var(--primary)_14%,var(--card))] text-[color-mix(in_oklab,var(--primary)_78%,var(--foreground))]',
'bg-[color-mix(in_oklab,var(--foreground)_8%,var(--card))] text-foreground',
'bg-[color-mix(in_oklab,var(--primary)_26%,var(--card))] text-[color-mix(in_oklab,var(--primary)_70%,var(--foreground))]',
'bg-[color-mix(in_oklab,var(--foreground)_16%,var(--card))] text-foreground',
];
const TONE: Record<Exclude<AvatarTone, 'auto'>, string> = {
accent: TONES[0],
neutral: TONES[1],
solid: 'bg-primary text-primary-foreground',
};
/* Online breathes a quiet ring, a stack pops in from the start when it scrolls into view */
const STYLES =
'.kitfx-av-live{animation:kitfx-av-live 2.8s var(--ease-kit) infinite}@keyframes kitfx-av-live{0%{opacity:.6;transform:scale(1)}70%,100%{opacity:0;transform:scale(2.2)}}' +
'.kitfx-av-in>.kitfx-av-item{animation:kitfx-av-in .55s cubic-bezier(.34,1.45,.64,1) both;animation-delay:calc(var(--i,0) * 60ms)}' +
'@keyframes kitfx-av-in{from{opacity:0;transform:translateX(-10px) scale(.7)}}' +
'@media (prefers-reduced-motion:reduce){.kitfx-av-live{animation:none;opacity:0}.kitfx-av-in>.kitfx-av-item{animation:none}}';
/** How far each avatar moves apart when the stack fans out, per size */
const FAN: Record<AvatarSize, number> = { xs: 3, sm: 4, md: 5, lg: 6, xl: 8, '2xl': 9 };
const STATUS: Record<AvatarStatus, string> = {
online: 'bg-success',
away: 'bg-warning',
busy: 'bg-destructive',
offline: 'bg-background shadow-[inset_0_0_0_1.5px_var(--subtle-foreground)]',
};
const TEXTS = {
en: { online: 'online', away: 'away', busy: 'busy', offline: 'offline', more: 'more', people: 'people' },
de: { online: 'online', away: 'abwesend', busy: 'beschäftigt', offline: 'offline', more: 'weitere', people: 'Personen' },
};
type AvatarTexts = (typeof TEXTS)['en'];
/** Up to two initials, from the first and the last word */
export function initials(name: string) {
const parts = name.trim().split(/\s+/).filter(Boolean);
if (!parts.length) return '?';
const first = parts[0][0] ?? '';
const last = parts.length > 1 ? parts[parts.length - 1][0] ?? '' : '';
return (first + last).toUpperCase();
}
const hash = (s: string) => [...s].reduce((h, c) => (h * 31 + c.charCodeAt(0)) >>> 0, 7);
export interface AvatarProps {
/** Full name, used for initials, the alt text and the tone */
name: string;
/** Image URL. Falls back to initials when it fails to load. */
src?: string;
/** Alt text, defaults to the name. Pass "" when the name is written next to the avatar. */
alt?: string;
/** Any drawn or custom image, shown instead of src */
image?: React.ReactNode;
size?: AvatarSize;
shape?: 'circle' | 'square';
status?: AvatarStatus;
/** Color of the initials. auto picks a calm tone from the name. */
tone?: AvatarTone;
/** Ring in the page background, for avatars on images or in stacks */
ring?: boolean;
labels?: Partial<AvatarTexts>;
className?: string;
}
/** A person as image or initials, with an optional presence dot. */
export function Avatar({ name, src, alt, image, size = 'md', shape = 'circle', status, tone = 'auto', ring, labels, className }: AvatarProps) {
const t = useTexts(TEXTS, labels);
const [failed, setFailed] = useState(false);
const s = SIZE[size];
const round = shape === 'circle' ? 'rounded-full' : size === 'xs' || size === 'sm' ? 'rounded-md' : 'rounded-lg';
const toneClass = tone === 'auto' ? TONES[hash(name) % TONES.length] : TONE[tone];
const showImage = image || (src && !failed);
return (
<span className={cn('relative inline-flex shrink-0 align-middle', s.box, className)}>
{/* The picture is cropped to the shape on purpose */}
<span
data-clip-ok
className={cn(
'relative flex size-full select-none items-center justify-center overflow-hidden font-semibold tracking-[-0.02em]',
round,
s.text,
!showImage && toneClass,
'shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--foreground)_8%,transparent)]',
ring && 'ring-2 ring-background',
)}
>
{image ? (
<span className="absolute inset-0 [&>*]:size-full" {...(alt === '' ? { 'aria-hidden': true } : { role: 'img', 'aria-label': alt ?? name })}>
{image}
</span>
) : src && !failed ? (
<img src={src} alt={alt ?? name} width={s.px} height={s.px} loading="lazy" decoding="async" onError={() => setFailed(true)} className="size-full object-cover" />
) : (
<>
<span aria-hidden>{initials(name)}</span>
{alt !== '' && <span className="sr-only">{name}</span>}
</>
)}
{showImage && <span aria-hidden className={cn('pointer-events-none absolute inset-0 shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--foreground)_10%,transparent)]', round)} />}
</span>
{status && (
<span
className={cn(
'absolute block rounded-full ring-2 ring-background',
s.dot,
shape === 'circle' ? 'bottom-[4%] right-[4%] translate-x-[15%] translate-y-[15%]' : '-bottom-0.5 -right-0.5',
STATUS[status],
)}
>
{status === 'online' && (
<>
<style href="kitfx-av" precedence="default">
{STYLES}
</style>
<span aria-hidden className="kitfx-av-live pointer-events-none absolute inset-0 rounded-full bg-success" />
</>
)}
<span className="sr-only">{t[status]}</span>
</span>
)}
</span>
);
}
export interface AvatarGroupPerson {
name: string;
src?: string;
image?: React.ReactNode;
}
export interface AvatarGroupProps {
people: AvatarGroupPerson[];
/** How many avatars to show before the +N chip */
max?: number;
size?: AvatarSize;
shape?: 'circle' | 'square';
/** Accessible name of the group, e.g. "Project team" */
label?: string;
labels?: Partial<AvatarTexts>;
className?: string;
}
const OVERLAP: Record<AvatarSize, string> = {
xs: '-ml-1.5',
sm: '-ml-2',
md: '-ml-2.5',
lg: '-ml-3',
xl: '-ml-4',
'2xl': '-ml-5',
};
/**
* Overlapping avatars with a +N chip. They pop in one after another when the stack scrolls into view,
* fan out on hover and keyboard focus, and the one under the pointer lifts.
*/
export function AvatarGroup({ people, max = 4, size = 'md', shape = 'circle', label, labels, className }: AvatarGroupProps) {
const t = useTexts(TEXTS, labels);
const shown = people.slice(0, max);
const rest = people.length - shown.length;
const s = SIZE[size];
const [ref, inView] = useInView<HTMLDivElement>();
const waited = useRef(false);
if (!inView) waited.current = true;
const entering = inView && waited.current;
const fan = (i: number) => ({ '--i': i, '--fan': `${i * FAN[size]}px` }) as React.CSSProperties;
return (
<div
ref={ref}
role="group"
aria-label={label ?? `${people.length} ${t.people}`}
className={cn('group/avatars flex items-center', !inView && '[&>.kitfx-av-item]:opacity-0', entering && 'kitfx-av-in', className)}
>
<style href="kitfx-av" precedence="default">
{STYLES}
</style>
{shown.map((p, i) => (
<span
key={p.name + i}
title={p.name}
className={cn(
'kitfx-av-item relative transition-[translate] duration-300 ease-[cubic-bezier(0.34,1.4,0.64,1)] group-hover/avatars:translate-x-[var(--fan)] group-focus-within/avatars:translate-x-[var(--fan)] motion-reduce:transition-none',
i > 0 && OVERLAP[size],
)}
style={{ ...fan(i), zIndex: shown.length - i }}
>
<span className="block transition-transform duration-200 ease-[var(--ease-kit)] hover:-translate-y-1 motion-reduce:transition-none">
<Avatar name={p.name} src={p.src} image={p.image} size={size} shape={shape} ring labels={labels} />
</span>
</span>
))}
{rest > 0 && (
<span
style={fan(shown.length)}
className={cn(
'kitfx-av-item relative inline-flex shrink-0 items-center justify-center bg-muted font-medium tabular-nums text-muted-foreground ring-2 ring-background transition-[translate] duration-300 ease-[cubic-bezier(0.34,1.4,0.64,1)] group-hover/avatars:translate-x-[var(--fan)] group-focus-within/avatars:translate-x-[var(--fan)] motion-reduce:transition-none',
OVERLAP[size],
s.box,
s.text,
shape === 'circle' ? 'rounded-full' : 'rounded-lg',
)}
>
<span aria-hidden>+{rest}</span>
<span className="sr-only">
{rest} {t.more}
</span>
</span>
)}
</div>
);
}
export interface AvatarLabelProps extends AvatarProps {
/** Second line, e.g. role or email */
description?: React.ReactNode;
}
/** Avatar with name and a second line, for lists, comments and authors. */
export function AvatarLabel({ description, className, ...avatar }: AvatarLabelProps) {
return (
<span className={cn('inline-flex min-w-0 items-center gap-3', className)}>
<Avatar alt="" {...avatar} />
<span className="min-w-0 leading-tight">
<span className="block truncate text-sm font-medium text-foreground">{avatar.name}</span>
{description && <span className="mt-0.5 block truncate text-[13px] text-muted-foreground">{description}</span>}
</span>
</span>
);
}Häufige Fragen
Wie installiere ich Avatar?
Mit aiw add @ai-website/avatar. Der Befehl legt die Datei components/ui/avatar.tsx in dein Projekt, der Code gehört danach dir. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Avatar?
4 Varianten, Größen und Arten, Gruppe, Anwesenheit und Profilkarte.
Kann ich die Texte von Avatar übersetzen?
Ja. Avatar bringt 6 eigene Texte auf Deutsch und Englisch mit, etwa „online“, „abwesend“ und „beschäftigt“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Avatar einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Avatar mit aiw_components und installieren sie mit aiw_add @ai-website/avatar.