<bpdm/ui />
Misc

Avatar

A user image with graceful initials/icon fallback — sizes, shapes, presence status, groups, in React (@bpdm/ui) and Angular (@bpdm/ng).

The Avatar shows a user's image and falls back gracefully — first to auto-tinted initials from the name, then to an icon. It supports six sizes, circle/square shapes, a presence dot, and an overlapping group with a +N overflow. In Angular it's <bpdm-avatar> / <bpdm-avatar-group>.

Usage

Pass name (drives the initials, auto-colour, and alt text) and an optional src. If the image is missing or fails to load, the initials show instead.

avatar-usage.tsx
import { Avatar } from '@bpdm/ui/avatar';

export function AvatarUsage() {
  return (
    <>
      <Avatar name="Aria Lindqvist" src="/aria.jpg" status="online" size="lg" />
      <Avatar name="Theo Brandt" size="lg" /> {/* no image → initials */}
    </>
  );
}
avatar-usage.ts
import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';

@Component({
  selector: 'avatar-usage',
  imports: [BpdmAvatar],
  templateUrl: './avatar-usage.html',
})
export class AvatarUsage {}
avatar-usage.html
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" status="online" size="lg" />
<bpdm-avatar name="Theo Brandt" size="lg" />

Initials

With no image, the avatar renders up to two initials from name, auto-tinted to a stable colour per name.

avatar-initials.tsx
import { Avatar } from '@bpdm/ui/avatar';

export function AvatarInitials() {
  return (
    <>
      <Avatar name="Aria Lindqvist" size="lg" />
      <Avatar name="Theo Brandt" size="lg" />
      <Avatar name="Lena Cho" size="lg" />
    </>
  );
}
avatar-initials.ts
import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';

@Component({
  selector: 'avatar-initials',
  imports: [BpdmAvatar],
  templateUrl: './avatar-initials.html',
})
export class AvatarInitials {}
avatar-initials.html
<bpdm-avatar name="Aria Lindqvist" size="lg" />
<bpdm-avatar name="Theo Brandt" size="lg" />
<bpdm-avatar name="Lena Cho" size="lg" />

Fallbacks

The chain is image → initials → icon. A broken src falls back to initials; with no name, it shows a custom icon if you pass one (React), otherwise a built-in user icon. Set colorful={false} for a neutral grey tint instead of the auto colour.

avatar-fallback.tsx
import { Building2 } from 'lucide-react';
import { Avatar } from '@bpdm/ui/avatar';

export function AvatarFallbacks() {
  return (
    <>
      <Avatar name="Clara Bauer" src="https://invalid.example/none.jpg" size="lg" /> {/* → initials */}
      <Avatar size="lg" /> {/* no name → built-in user icon */}
      <Avatar size="lg" icon={<Building2 />} /> {/* custom fallback icon */}
      <Avatar name="JD" colorful={false} size="lg" /> {/* neutral */}
    </>
  );
}

icon is React-only; Angular always uses the built-in user icon when there's no image and no name.

avatar-fallback.ts
import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';

@Component({
  selector: 'avatar-fallback',
  imports: [BpdmAvatar],
  templateUrl: './avatar-fallback.html',
})
export class AvatarFallback {}
avatar-fallback.html
<bpdm-avatar name="Clara Bauer" src="https://invalid.example/none.jpg" size="lg" />
<bpdm-avatar size="lg" />
<bpdm-avatar name="JD" [colorful]="false" size="lg" />

Sizes

Six sizes: xs, sm, md (default), lg, xl, 2xl.

avatar-sizes.tsx
import { Avatar } from '@bpdm/ui/avatar';

export function AvatarSizes() {
  return (
    <>
      <Avatar name="Aria Lindqvist" src="/aria.jpg" size="xs" />
      <Avatar name="Aria Lindqvist" src="/aria.jpg" size="sm" />
      <Avatar name="Aria Lindqvist" src="/aria.jpg" size="md" />
      <Avatar name="Aria Lindqvist" src="/aria.jpg" size="lg" />
      <Avatar name="Aria Lindqvist" src="/aria.jpg" size="xl" />
      <Avatar name="Aria Lindqvist" src="/aria.jpg" size="2xl" />
    </>
  );
}
avatar-sizes.ts
import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';

@Component({
  selector: 'avatar-sizes',
  imports: [BpdmAvatar],
  templateUrl: './avatar-sizes.html',
})
export class AvatarSizes {}
avatar-sizes.html
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" size="xs" />
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" size="sm" />
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" size="md" />
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" size="lg" />
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" size="xl" />
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" size="2xl" />

Shapes

circle (default) or square (a rounded square).

avatar-shapes.tsx
import { Avatar } from '@bpdm/ui/avatar';

export function AvatarShapes() {
  return (
    <>
      <Avatar name="Aria Lindqvist" src="/aria.jpg" shape="circle" size="lg" />
      <Avatar name="Aria Lindqvist" src="/aria.jpg" shape="square" size="lg" />
    </>
  );
}
avatar-shapes.ts
import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';

@Component({
  selector: 'avatar-shapes',
  imports: [BpdmAvatar],
  templateUrl: './avatar-shapes.html',
})
export class AvatarShapes {}
avatar-shapes.html
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" shape="circle" size="lg" />
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" shape="square" size="lg" />

Status

A presence dot via status: online, busy, away, or offline.

avatar-status.tsx
import { Avatar } from '@bpdm/ui/avatar';

export function AvatarStatus() {
  return (
    <>
      <Avatar name="Aria Lindqvist" src="/aria.jpg" status="online" size="lg" />
      <Avatar name="Theo Brandt" status="busy" size="lg" />
      <Avatar name="Lena Cho" src="/lena.jpg" status="away" size="lg" />
      <Avatar name="Mateo Silva" status="offline" size="lg" />
    </>
  );
}
avatar-status.ts
import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';

@Component({
  selector: 'avatar-status',
  imports: [BpdmAvatar],
  templateUrl: './avatar-status.html',
})
export class AvatarStatus {}
avatar-status.html
<bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" status="online" size="lg" />
<bpdm-avatar name="Theo Brandt" status="busy" size="lg" />
<bpdm-avatar name="Lena Cho" src="/lena.jpg" status="away" size="lg" />
<bpdm-avatar name="Mateo Silva" status="offline" size="lg" />

Group

AvatarGroup overlaps avatars and collapses the rest past max into a +N tile; each lifts on hover. In Angular, pass the people via the users input.

+2
+2
avatar-group.tsx
import { Avatar, AvatarGroup } from '@bpdm/ui/avatar';

export function Team() {
  return (
    <AvatarGroup max={4}>
      <Avatar name="Aria Lindqvist" src="/aria.jpg" />
      <Avatar name="Theo Brandt" src="/theo.jpg" />
      <Avatar name="Lena Cho" src="/lena.jpg" />
      <Avatar name="Mateo Silva" />
      <Avatar name="Ines Vidal" />
      <Avatar name="Sam Reyes" />
    </AvatarGroup>
  );
}
avatar-group.ts
import { Component } from '@angular/core';
import { BpdmAvatarGroup, type AvatarGroupUser } from '@bpdm/ng';

@Component({
  selector: 'team',
  imports: [BpdmAvatarGroup],
  templateUrl: './avatar-group.html',
})
export class Team {
  readonly people: AvatarGroupUser[] = [
    { name: 'Aria Lindqvist', src: '/aria.jpg' },
    { name: 'Theo Brandt', src: '/theo.jpg' },
    { name: 'Lena Cho', src: '/lena.jpg' },
    { name: 'Mateo Silva' },
    { name: 'Ines Vidal' },
    { name: 'Sam Reyes' },
  ];
}
avatar-group.html
<bpdm-avatar-group [users]="people" [max]="4" />

Notification badge

Compose with NotificationBadge to overlay a count or dot on the corner — for unread messages, presence, and the like. The count caps at max (e.g. 99+).

499+
avatar-badge.tsx
import { Avatar } from '@bpdm/ui/avatar';
import { NotificationBadge } from '@bpdm/ui/badge';

export function Badged() {
  return (
    <>
      <NotificationBadge count={4}>
        <Avatar name="Aria Lindqvist" src="/aria.jpg" size="lg" />
      </NotificationBadge>
      <NotificationBadge count={128} max={99}>
        <Avatar name="Lena Cho" src="/lena.jpg" size="lg" />
      </NotificationBadge>
      <NotificationBadge dot variant="success">
        <Avatar name="Theo Brandt" size="lg" />
      </NotificationBadge>
    </>
  );
}
avatar-badge.ts
import { Component } from '@angular/core';
import { BpdmAvatar, BpdmNotificationBadge } from '@bpdm/ng';

@Component({
  selector: 'badged',
  imports: [BpdmAvatar, BpdmNotificationBadge],
  templateUrl: './avatar-badge.html',
})
export class Badged {}
avatar-badge.html
<bpdm-notification-badge [count]="4">
  <bpdm-avatar name="Aria Lindqvist" src="/aria.jpg" size="lg" />
</bpdm-notification-badge>
<bpdm-notification-badge [count]="128" [max]="99">
  <bpdm-avatar name="Lena Cho" src="/lena.jpg" size="lg" />
</bpdm-notification-badge>
<bpdm-notification-badge dot variant="success">
  <bpdm-avatar name="Theo Brandt" size="lg" />
</bpdm-notification-badge>

Internationalization

Almost everything the avatar shows is author-supplied — the name (which drives the initials and the alt text) and any alt you pass are your own content, so you translate them exactly like the rest of your app. The strings the component owns are the presence-status labels — the screen-reader name of the status dot (online / offline / busy / away) — and the AvatarGroup overflow label (more, with {count} interpolated for the +N tile). Set them via a single messages object; it's a Partial<AvatarMessages> merged over the English defaults, so override one key or several:

  • online / offline / busy / away — the status dot's accessible name per presence value. Defaults "Online" / "Offline" / "Busy" / "Away".

  • more — the AvatarGroup overflow tile's label. {count} is interpolated. Default "{count} more".

  • React — pass messages to <Avatar messages={{ … }} /> and/or <AvatarGroup messages={{ … }} />.

  • Angular — bind [messages] on <bpdm-avatar> and/or <bpdm-avatar-group>.

A messages object set on the group is forwarded to its child avatars, so you localise the status labels and the overflow label together in one place.

The avatar mirrors under dir="rtl" with no extra prop — the presence dot moves to the inline-end bottom corner and the group stack overlaps in the reading direction.

The example below localises only the built-in status / overflow strings to German via messages — the names stay your own content:

avatar-i18n.tsx
import { Avatar, AvatarGroup, type AvatarMessages } from '@bpdm/ui/avatar';

// Only the built-in status + overflow strings are translated; names stay as-is.
const messages: Partial<AvatarMessages> = {
  online: 'Online',
  offline: 'Offline',
  busy: 'Beschäftigt',
  away: 'Abwesend',
  more: '{count} weitere',
};

export function Team() {
  return (
    <AvatarGroup max={4} messages={messages}>
      <Avatar name="Aria Lindqvist" src="/aria.jpg" status="online" />
      <Avatar name="Theo Brandt" status="busy" />
      <Avatar name="Lena Cho" src="/lena.jpg" status="away" />
      <Avatar name="Mateo Silva" status="offline" />
      <Avatar name="Ines Vidal" />
      <Avatar name="Sam Reyes" />
    </AvatarGroup>
  );
}
team.ts
import { Component } from '@angular/core';
import { BpdmAvatarGroup, type AvatarGroupUser, type AvatarMessages } from '@bpdm/ng';

@Component({
  selector: 'team',
  imports: [BpdmAvatarGroup],
  templateUrl: './team.html',
})
export class Team {
  // Only the built-in status + overflow strings are translated; names stay as-is.
  readonly messages: Partial<AvatarMessages> = {
    online: 'Online',
    offline: 'Offline',
    busy: 'Beschäftigt',
    away: 'Abwesend',
    more: '{count} weitere',
  };

  readonly people: AvatarGroupUser[] = [
    { name: 'Aria Lindqvist', src: '/aria.jpg', status: 'online' },
    { name: 'Theo Brandt', status: 'busy' },
    { name: 'Lena Cho', src: '/lena.jpg', status: 'away' },
    { name: 'Mateo Silva', status: 'offline' },
    { name: 'Ines Vidal' },
    { name: 'Sam Reyes' },
  ];
}
team.html
<bpdm-avatar-group [users]="people" [max]="4" [messages]="messages" />

API

Avatar / <bpdm-avatar>

PropTypeDefaultDescription
namestringDrives the initials, auto-colour, and alt text.
srcstringImage URL; falls back to initials, then icon.
altstringnameImage alt text.
iconReactNodeuser glyphReact only — custom fallback when there's no image and no name. Angular shows the built-in user icon.
sizexs | sm | md | lg | xl | 2xlmdAvatar size.
shapecircle | squarecircleCircle or rounded square.
statusonline | busy | away | offlinePresence dot; its screen-reader name comes from messages.
colorfulbooleantrueAuto-tint initials from the name; false → neutral.
ringbooleanfalseDraw a background-coloured ring around the avatar (used by AvatarGroup). Angular: [ring].
messagesPartial<AvatarMessages>EnglishOverride the translatable strings — the status dot labels (see below). Angular: [messages].
classNamestringReact — extra classes on the root. Angular: use class.

AvatarGroup / <bpdm-avatar-group>

PropTypeDefaultDescription
childrenReactNodeReact — the <Avatar> elements to stack. Angular uses [users] instead.
usersAvatarGroupUser[]Angular{ name?, src?, status? }[] (React composes <Avatar> children instead).
maxnumberShow at most this many; the rest collapse into +N. Angular: [max].
sizeAvatarSizemdApplied to every avatar and the overflow tile.
messagesPartial<AvatarMessages>EnglishOverride the translatable strings — the +N overflow label + status dot labels; forwarded to child avatars (see below). Angular: [messages].
classNamestringReact — extra classes on the wrapper. Angular: use class.

NotificationBadge / <bpdm-notification-badge>

PropTypeDefaultDescription
countnumberNumber to display; omit with dot for a plain indicator.
maxnumber99Cap, e.g. 99+.
dotbooleanfalseShow a dot instead of a number.
showZerobooleanfalseRender when count is 0.
variantBadgeVariantdestructiveIndicator colour.

App-wide copy — messages

Localise the built-in strings — the presence-status labels and the AvatarGroup overflow label — in one place (see Internationalization). It's a Partial<AvatarMessages> merged over the English defaults, so override one key or several. A messages set on the group is forwarded to its child avatars.

  • React — the messages prop on <Avatar> and/or <AvatarGroup>.
  • Angular — the [messages] input on <bpdm-avatar> and/or <bpdm-avatar-group>.
AvatarMessages keyDefaultDescription
onlineOnlineAccessible name of the status dot when status="online".
offlineOfflineAccessible name of the status dot when status="offline".
busyBusyAccessible name of the status dot when status="busy".
awayAwayAccessible name of the status dot when status="away".
more{count} moreAvatarGroup overflow tile label; {count} is interpolated.

Accessibility

  • The avatar shows an image described by alt (which defaults to name), falling back to initials and then an icon — but when it falls back it still exposes the person's name to screen readers, so the identity is never lost and a missing image never produces a broken-image read-out.
  • The presence dot is exposed as an image (role="img") with a localised label from messages (default "Online" / "Offline" / "Busy" / "Away"), so status is announced in words — never conveyed by colour alone.
  • The default user icon (shown when there's no image and no name) is decorative and aria-hidden, so it isn't announced.
  • The AvatarGroup's overflow tile announces its count via messages.more (default "{count} more"), so the hidden people are conveyed in text.
  • Everything mirrors under dir="rtl" — the presence dot moves to the inline-end bottom corner and the group stack overlaps in the reading direction.
  • Avatars are non-interactive by default. For an avatar that acts as a control (a menu trigger, a link), wrap it in a <button> / <a> with an accessible name rather than relying on the image.
  • NotificationBadge overlays a count visually; convey the same information in the trigger's accessible name (e.g. aria-label="Messages, 4 unread").

On this page