Zoeken...  ⌘K GitHub

Avatar UI Elements

Avatar met foto of initialen fallback. Groepweergave met overlap. Status indicator (online/offline/away/busy).

/avatar
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