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.
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 */}
</>
);
}import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';
@Component({
selector: 'avatar-usage',
imports: [BpdmAvatar],
templateUrl: './avatar-usage.html',
})
export class AvatarUsage {}<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.
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" />
</>
);
}import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';
@Component({
selector: 'avatar-initials',
imports: [BpdmAvatar],
templateUrl: './avatar-initials.html',
})
export class AvatarInitials {}<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.
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.
import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';
@Component({
selector: 'avatar-fallback',
imports: [BpdmAvatar],
templateUrl: './avatar-fallback.html',
})
export class AvatarFallback {}<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.
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" />
</>
);
}import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';
@Component({
selector: 'avatar-sizes',
imports: [BpdmAvatar],
templateUrl: './avatar-sizes.html',
})
export class AvatarSizes {}<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).
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" />
</>
);
}import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';
@Component({
selector: 'avatar-shapes',
imports: [BpdmAvatar],
templateUrl: './avatar-shapes.html',
})
export class AvatarShapes {}<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.
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" />
</>
);
}import { Component } from '@angular/core';
import { BpdmAvatar } from '@bpdm/ng';
@Component({
selector: 'avatar-status',
imports: [BpdmAvatar],
templateUrl: './avatar-status.html',
})
export class AvatarStatus {}<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.
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>
);
}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' },
];
}<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+).
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>
</>
);
}import { Component } from '@angular/core';
import { BpdmAvatar, BpdmNotificationBadge } from '@bpdm/ng';
@Component({
selector: 'badged',
imports: [BpdmAvatar, BpdmNotificationBadge],
templateUrl: './avatar-badge.html',
})
export class Badged {}<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
messagesto<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:
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>
);
}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' },
];
}<bpdm-avatar-group [users]="people" [max]="4" [messages]="messages" />API
Avatar / <bpdm-avatar>
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Drives the initials, auto-colour, and alt text. |
src | string | — | Image URL; falls back to initials, then icon. |
alt | string | name | Image alt text. |
icon | ReactNode | user glyph | React only — custom fallback when there's no image and no name. Angular shows the built-in user icon. |
size | xs | sm | md | lg | xl | 2xl | md | Avatar size. |
shape | circle | square | circle | Circle or rounded square. |
status | online | busy | away | offline | — | Presence dot; its screen-reader name comes from messages. |
colorful | boolean | true | Auto-tint initials from the name; false → neutral. |
ring | boolean | false | Draw a background-coloured ring around the avatar (used by AvatarGroup). Angular: [ring]. |
messages | Partial<AvatarMessages> | English | Override the translatable strings — the status dot labels (see below). Angular: [messages]. |
className | string | — | React — extra classes on the root. Angular: use class. |
AvatarGroup / <bpdm-avatar-group>
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | React — the <Avatar> elements to stack. Angular uses [users] instead. |
users | AvatarGroupUser[] | — | Angular — { name?, src?, status? }[] (React composes <Avatar> children instead). |
max | number | — | Show at most this many; the rest collapse into +N. Angular: [max]. |
size | AvatarSize | md | Applied to every avatar and the overflow tile. |
messages | Partial<AvatarMessages> | English | Override the translatable strings — the +N overflow label + status dot labels; forwarded to child avatars (see below). Angular: [messages]. |
className | string | — | React — extra classes on the wrapper. Angular: use class. |
NotificationBadge / <bpdm-notification-badge>
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | — | Number to display; omit with dot for a plain indicator. |
max | number | 99 | Cap, e.g. 99+. |
dot | boolean | false | Show a dot instead of a number. |
showZero | boolean | false | Render when count is 0. |
variant | BadgeVariant | destructive | Indicator 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
messagesprop on<Avatar>and/or<AvatarGroup>. - Angular — the
[messages]input on<bpdm-avatar>and/or<bpdm-avatar-group>.
AvatarMessages key | Default | Description |
|---|---|---|
online | Online | Accessible name of the status dot when status="online". |
offline | Offline | Accessible name of the status dot when status="offline". |
busy | Busy | Accessible name of the status dot when status="busy". |
away | Away | Accessible name of the status dot when status="away". |
more | {count} more | AvatarGroup overflow tile label; {count} is interpolated. |
Accessibility
- The avatar shows an image described by
alt(which defaults toname), falling back to initials and then an icon — but when it falls back it still exposes the person'snameto 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 frommessages(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. NotificationBadgeoverlays a count visually; convey the same information in the trigger's accessible name (e.g.aria-label="Messages, 4 unread").
Toast
Transient notifications fired imperatively from anywhere — variants, descriptions, actions, promises, and six dock positions, in React (@bpdm/ui) and Angular (@bpdm/ng).
Badge
A compact label for status, counts and tags — tones, appearances, status dots, removable chips, and corner notifications, in React (@bpdm/ui) and Angular (@bpdm/ng).