Avatar & Badge
oge-avatar, oge-avatar-group and oge-badge. An avatar runs its own fallback chain — image, initials, icon — and names itself as one role="img", presence included; a group collapses its surplus into "+N".
A badge glyph is always aria-hidden: a bare "5" next to a button says nothing. What a screen reader hears is the description — "5 new items" — wired into the wrapped control's aria-describedby, so it is read with the control's name.
Image, initials & icon
The fallback chain runs on its own: the image while src loads, the initials (explicit, or derived from name — first and last word, upper-cased in the locale) when it fails, the person icon without either. A new src gets a fresh attempt.
imageFailed → —
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeAvatar } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeAvatar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The fallback chain runs on its own: an image while src loads, the
initials (explicit or derived from name) when it fails, the icon
without either. The host is role="img" named by the name. -->
<oge-avatar name="OGE UI" src="/logo.png" />
<oge-avatar name="Ada Lovelace" src="/missing-avatar.png" (imageFailed)="failed.set($event.src)" />
<oge-avatar name="Grace Hopper" initials="GH" />
<oge-avatar ariaLabel="Guest" />
`,
})
export class Demo {
protected readonly failed = signal('');
}Sizes & shapes
Five sizes (24 to 64 px) on one host-local knob, so the image, the initials and the icon all fill the same box and the fallback never shifts layout. rounded uses the --oge-radius-lg token.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeAvatar } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeAvatar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- xs 24 · sm 32 · md 40 · lg 48 · xl 64 px; circle, rounded, square. -->
@for (size of sizes; track size) {
<oge-avatar name="Ada Lovelace" [size]="size" />
}
<oge-avatar name="Grace Hopper" size="lg" shape="rounded" />
<oge-avatar name="Alan Turing" size="lg" shape="square" />
`,
})
export class Demo {
protected readonly sizes = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
}Presence status
The dot is aria-hidden; the presence joins the accessible name ("Ada Lovelace (Online)") through the catalog’s withStatus pattern. Offline is a hollow ring, so the state never rides on colour alone. Beside a visible name, decorative hides the avatar from assistive technology.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeAvatar } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeAvatar],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The dot is aria-hidden; the presence joins the accessible name
("Ada Lovelace (Online)"). Offline is a hollow ring, so the state never
rides on colour alone. Next to a visible name, decorative avoids
reading it twice. -->
<oge-avatar name="Ada Lovelace" status="online" />
<oge-avatar name="Grace Hopper" status="away" />
<oge-avatar name="Alan Turing" status="busy" />
<oge-avatar name="Edsger Dijkstra" status="offline" />
<span class="person">
<oge-avatar name="Ada Lovelace" size="sm" [decorative]="true" />
Ada Lovelace
</span>
`,
})
export class Demo {}Avatar group
max counts the rendered circles including the surplus one, so the row never grows past it; total adds the rest of a partially loaded list. The "+N" avatar is a role="img" named "N more", and the group a labelled role="group".
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeAvatar, OgeAvatarGroup } from '@oge-ui/layout';
import type { OgeAvatarItem } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeAvatar, OgeAvatarGroup],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- max counts the rendered circles INCLUDING the surplus one, so the
row never grows past it. total is the full population of a partially
loaded list. Projected children follow the items. -->
<oge-avatar-group [items]="team" [max]="4" ariaLabel="Project team" />
<oge-avatar-group [items]="reviewers" [max]="4" [total]="24" size="sm" ariaLabel="Reviewers" />
<oge-avatar-group [overlap]="false" ariaLabel="On call">
<oge-avatar name="Ada Lovelace" status="online" />
<oge-avatar name="Grace Hopper" status="busy" />
</oge-avatar-group>
`,
})
export class Demo {
protected readonly team: OgeAvatarItem[] = [
{ key: 1, name: 'Ada Lovelace', status: 'online' },
{ key: 2, name: 'Grace Hopper' },
{ key: 3, name: 'Alan Turing', status: 'away' },
{ key: 4, name: 'Edsger Dijkstra' },
{ key: 5, name: 'Barbara Liskov' },
{ key: 6, name: 'Donald Knuth' },
];
// three of 24 reviewers are loaded — total counts the rest into "+N"
protected readonly reviewers = this.team.slice(0, 3);
}Badge on an element
Content inside the badge becomes its anchor. The glyph sits on a logical corner (mirrors in RTL) and is aria-hidden; the wrapped control’s aria-describedby points at the description, so a screen reader hears "Inbox, 5 new items". Above max it shows 99+; overlap="circle" sits on a round host’s outline.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeAvatar, OgeBadge } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeAvatar, OgeBadge],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The glyph is aria-hidden decoration; the wrapped control's
aria-describedby points at the description, so a screen reader hears
"Inbox, 5 new items". Above max it shows 99+. -->
<oge-badge [value]="unread()">
<button type="button" (click)="unread.set(unread() + 1)">Inbox</button>
</oge-badge>
<oge-badge [value]="120" severity="accent" position="bottom-end">
<button type="button">Notifications</button>
</oge-badge>
<!-- overlap="circle" sits on the outline of a round host -->
<oge-badge [dot]="true" severity="success" overlap="circle" description="Available">
<oge-avatar name="Grace Hopper" />
</oge-badge>
`,
})
export class Demo {
protected readonly unread = signal(5);
}Standalone badge & dot
Without content the badge is an inline pill with a visually hidden description. A zero count hides unless showZero; invisible force-hides; a string value ("Beta") reads itself.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeBadge } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeBadge],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Without content the badge is an inline pill with a visually hidden
description. A zero count hides unless showZero; invisible force-hides. -->
<span>Messages <oge-badge [value]="3" /></span>
<span>Plan <oge-badge value="Beta" severity="accent" /></span>
<span>Updates <oge-badge [dot]="true" description="New updates" /></span>
<span>Errors <oge-badge [value]="0" [showZero]="true" severity="neutral" size="sm" /></span>
`,
})
export class Demo {}Live announcements
announce speaks later changes through a polite live region. The initial value is part of the page and is never announced — only what changes while the reader is there.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeBadge } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeBadge],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- announce speaks later changes through a polite live region; the
initial value is part of the page and is never announced. -->
<oge-badge [value]="cart()" [announce]="true" description="{{ cart() }} items in cart">
<button type="button">Cart</button>
</oge-badge>
<button type="button" (click)="cart.set(cart() + 1)">Add item</button>
`,
})
export class Demo {
protected readonly cart = signal(1);
}Configuration
Application- or component-scoped defaults. Every user-facing string — presence labels, "N more", the badge descriptions and the "99+" pattern — lives in a messages catalog, and the ready-made language packs cover it.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeAvatar, OgeBadge, provideOgeAvatarConfig, provideOgeBadgeConfig } from '@oge-ui/layout';
// Application- or component-scoped defaults; every user-facing string
// (presence labels, "N more", the badge descriptions) is in messages.
export const avatarProviders = [
provideOgeAvatarConfig({ size: 'sm', messages: { busy: 'In a meeting' } }),
provideOgeBadgeConfig({ max: 9, severity: 'accent' }),
];
@Component({
selector: 'demo-root',
imports: [OgeAvatar, OgeBadge],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-avatar name="Ada Lovelace" status="busy" />
<oge-badge [value]="12" />
`,
})
export class Demo {}