Avatar UI Elements
Avatar met foto of initialen fallback. Groepweergave met overlap. Status indicator (online/offline/away/busy).
src/components/ui/Avatar.astro
---
/**
* Avatar + AvatarGroup
* Ronde avatar met fallback initialen. Online indicator optioneel.
* AvatarGroup: gestapeld met max-count overflow.
*/
interface AvatarItem {
src?: string;
alt?: string;
name?: string;
color?: string;
}
interface Props {
src?: string;
alt?: string;
name?: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
status?: 'online' | 'offline' | 'away' | 'busy';
color?: string;
square?: boolean;
/** AvatarGroup mode */
group?: AvatarItem[];
groupMax?: number;
groupSize?: 'xs' | 'sm' | 'md' | 'lg';
}
const {
src,
alt,
name,
size = 'md',
status,
color,
square = false,
group,
groupMax = 4,
groupSize = 'md',
} = Astro.props;
function initials(n?: string) {
if (!n) return '?';
return n.split(' ').slice(0, 2).map(w => w[0]).join('').toUpperCase();
}
const defaultColors = ['#6366f1','#8b5cf6','#ec4899','#f59e0b','#10b981','#3b82f6','#ef4444'];
function colorFor(n?: string) {
if (!n) return '#6b7280';
const idx = n.charCodeAt(0) % defaultColors.length;
return defaultColors[idx];
}
---
{group ? (
<!-- AvatarGroup -->
<div class:list={['av-group', `av-group--${groupSize}`]} data-component="avatar-group">
{group.slice(0, groupMax).map(a => (
<div class="av av--group" title={a.name ?? a.alt}>
{a.src ? (
<img src={a.src} alt={a.alt ?? a.name ?? ''} class="av__img" />
) : (
<span class="av__initials" style={`background:${a.color ?? colorFor(a.name)}`}>
{initials(a.name)}
</span>
)}
</div>
))}
{group.length > groupMax && (
<div class="av av--overflow av--group">
<span class="av__overflow-count">+{group.length - groupMax}</span>
</div>
)}
</div>
) : (
<!-- Single Avatar -->
<div
class:list={['av', `av--${size}`, { 'av--square': square, [`av--status-${status}`]: !!status }]}
data-component="avatar"
title={name ?? alt}
>
{src ? (
<img src={src} alt={alt ?? name ?? ''} class="av__img" />
) : (
<span class="av__initials" style={`background:${color ?? colorFor(name)}`}>
{initials(name)}
</span>
)}
{status && <span class="av__status" aria-label={status}></span>}
</div>
)}
<style>
/* === SINGLE AVATAR === */
.av {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
background: rgba(0,0,0,0.08);
}
.av--square { border-radius: var(--radius, 0.5rem); }
/* Sizes */
.av--xs { width: 24px; height: 24px; font-size: 0.5rem; }
.av--sm { width: 32px; height: 32px; font-size: 0.625rem; }
.av--md { width: 40px; height: 40px; font-size: 0.875rem; }
.av--lg { width: 56px; height: 56px; font-size: 1.125rem; }
.av--xl { width: 80px; height: 80px; font-size: 1.5rem; }
.av__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.av__initials {
color: #fff;
font-weight: 700;
letter-spacing: -0.03em;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: inherit;
}
/* Status dot */
.av__status {
position: absolute;
bottom: 1px;
right: 1px;
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid var(--color-bg, #fff);
}
.av--xs .av__status { width: 7px; height: 7px; }
.av--lg .av__status { width: 13px; height: 13px; }
.av--xl .av__status { width: 16px; height: 16px; }
.av--status-online .av__status { background: #22c55e; }
.av--status-offline .av__status { background: #9ca3af; }
.av--status-away .av__status { background: #f59e0b; }
.av--status-busy .av__status { background: #ef4444; }
/* === AVATAR GROUP === */
.av-group {
display: inline-flex;
flex-direction: row-reverse;
}
.av-group--xs .av { width: 24px; height: 24px; font-size: 0.5rem; }
.av-group--sm .av { width: 32px; height: 32px; font-size: 0.625rem; }
.av-group--md .av { width: 40px; height: 40px; font-size: 0.875rem; }
.av-group--lg .av { width: 56px; height: 56px; font-size: 1.125rem; }
.av--group {
border: 2px solid var(--color-bg, #fff);
margin-left: -8px;
}
.av-group .av--group:last-child { margin-left: 0; }
.av--overflow {
background: rgba(0,0,0,0.08);
}
.av__overflow-count {
font-size: 0.6875rem;
font-weight: 700;
color: var(--color-muted, #6b7280);
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
name | string | — | Naam voor initialen fallback en alt tekst |
src | string | — | Foto URL |
size | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Grootte |
status | 'online' | 'offline' | 'away' | 'busy' | — | Status indicator |
group | { name: string; src?: string }[] | — | Array voor groepweergave met overlap |
shape | 'circle' | 'square' | 'circle' | Vorm |
* = verplicht