Sizes, and what fills them
A name becomes initials; a src becomes the picture; neither becomes a silhouette. The three sizes ride the library's own axis, so an avatar beside a field lines up.
The picture beside a name, with initials and then a silhouette standing in when there is no picture.
@pacit/components/avatarpct-avatarNo APG pattern applies, and none is invented — the reasoning is under Accessibility ↓
import { Component } from '@angular/core';
import { PctAvatar } from '@pacit/components/avatar';
/** A name becomes initials; a portrait takes over when you hand one in via `src`. */
@Component({
selector: 'demo-avatar',
imports: [PctAvatar],
styles:
':host { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }',
template: `
<pct-avatar name="Ada Lovelace" size="sm" />
<pct-avatar name="Grace Hopper" />
<pct-avatar name="Margaret Hamilton" size="lg" />
`,
})
export class AvatarDemo {}avatar.demo.ts — the same file runs here and is highlighted under Code
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
import { PctAvatar } from '@pacit/components/avatar';| Exported | What it is |
|---|---|
PctAvatarcomponentpct-avatar | An avatar: the picture beside a name — and never the name itself. |
<pct-avatar name="Ada Lovelace" />Each example is the component itself, not a screenshot. Open the code under any of them.
A name becomes initials; a src becomes the picture; neither becomes a silhouette. The three sizes ride the library's own axis, so an avatar beside a field lines up.
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
An avatar: the picture beside a name — and never the name itself.
| Name | Type | What it does |
|---|---|---|
namedefault '' | string | The name the initials are taken from. It is a SOURCE FOR A DRAWING, not an accessible name — the host is hidden, and the name a user hears belongs to the text or the control beside the picture (0052). |
srcdefault '' | string | The picture's URL. Empty means none — the initials (or the silhouette) stand instead. |
sizedefault config.defaultSize | PctSize | Size on the shared control axis; from the global configuration by default. |
(click), (focus), the form's submit — are the platform's, and two-way models above carry the rest. The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
| Attribute | Bound to | Note |
|---|---|---|
class | pct-avatar | |
aria-hidden | true | Decoration beside the meaning, like the icon it may fall back to drawing. Nothing below it can take focus and the one image is alt="", so hiding the subtree costs a reader nothing and saves them hearing a name twice (req-a11y-built-in, 0052). |
data-pct-size | size() |
Two ways in, both stable. No class names to depend on.
In a test, PctAvatarHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.
| Part | Selector | What it is |
|---|---|---|
image | [data-pct-part="image"] | the picture, when a source is given |
initials | [data-pct-part="initials"] | the letters drawn from the name |
silhouette | [data-pct-part="silhouette"] | the placeholder when there is neither |
Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.
| Token | Styles |
|---|---|
--pct-avatar-bg{pct.surface-100}#f1f5f9 · #1e293b | Background of the avatar. |
--pct-avatar-border{pct.border}#e2e8f0 · #334155 | Edge colour of the avatar. |
--pct-avatar-fg{pct.text}#0f172a · #ffffff | Text colour of the initials. |
--pct-avatar-font-size{pct.font.size.md}14px | Font size of the avatar. |
--pct-avatar-font-size-lg{pct.font.size.lg}16px | Font size of the avatar at size lg. |
--pct-avatar-font-size-sm{pct.font.size.sm}13px | Font size of the avatar at size sm. |
--pct-avatar-icon-fg{pct.text-muted}#334155 · #e2e8f0 | Text colour of the icon. |
--pct-avatar-radius999px | Corner radius of the avatar. |
--pct-avatar-size{pct.control.height.md}36px | Size of the avatar. |
--pct-avatar-size-lg{pct.control.height.lg}44px | Size of the avatar at size lg. |
--pct-avatar-size-sm{pct.control.height.sm}28px | Size of the avatar at size sm. |
A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.
[data-theme='brand'] {
--pct-avatar-bg: #0f766e;
--pct-avatar-fg: #ffffff;
--pct-avatar-border: #0f766e;
} Pattern: none — an avatar is decoration, all the way down (0052). The host is aria-hidden="true" outright — the third component after pct-icon and pct-skeleton — because where an avatar stands the name is already text a reader says, and a second announcement is noise. Named in the class JSDoc.
| key | effect | test |
|---|
Empty by design: nothing here can be landed on at all. The subtree is hidden from the accessibility tree, so anything focusable in it would be axe's aria-hidden-focus — apps/sandbox-e2e/src/avatar.spec.ts asks every element in the component to focus itself and counts zero, in three engines, and tools/check-aria.mjs point 8 refuses the general case at build time (req-api-platform).
Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
libs/components/avatar/src/avatar.tsapps/sandbox-e2e/src/avatar.spec.ts — the empty map is the claim: no element in the component takes focus in any of the three enginesapps/sandbox-e2e/src/a11y.spec.ts — /avatar in SBX_ROUTES, so the WCAG 2.2 AA sweep takes it; aria-hidden-focus is the rule that would bite, and the subtree holds nothing to land onapps/sandbox-e2e/src/visual.spec.ts — avatar-chain and avatar-chain-rtl: all three links of the chain in one frameforced-colors: active — no state carried by colour alonelibs/components/avatar/src/avatar.scss — the fill goes, the ring rides on CanvasText and the initials and silhouette take the same word of the user palette. The reading: apps/sandbox-e2e/src/forced-colors.spec.tsprefers-reduced-motion — duration from a tokennot applicable — the component has no motion at all: no transition, no loop, so the preference has nothing to slowreq-a11y-touch sets has nothing to apply to--pct-control-height-*libs/tokens/src/component.avatar.json — size IS the control axis on both dimensions, so an avatar lines up with the field beside it by definition; apps/sandbox-e2e/src/avatar.spec.ts reads 28/36/44 off the boxes and equal sides off every one (the circle's arithmetic, and the anti-ellipse one)dir="rtl" screenshotlibs/components/avatar/src/avatar.scss — logical properties only, and nothing directional to mirror: a circle, centred content, no order. The screenshot: avatar-chain-rtlNG05xxapps/sandbox-e2e/src/hydration.spec.ts — /avatar in SBX_ROUTESFormValueControllibs/components/parts.snapshot.md, tools/check-parts.mjs — the Parts row abovecontrast.policy.jsonlibs/tokens/src/contrast.policy.json — three entries: the initials (AA error), the silhouette (UI warn, hidden decoration a sighted user still needs to see), the ring (UI warn — the chips' entry one component over, and here load-bearing three ways); libs/tokens/tokens.snapshot.mdPCT_TEXTSnot applicable — the component draws no text of its own; the initials are the consumer's name, redrawnlibs/components/size.snapshot.md — ./avatar on ./core and ./icon (the silhouette goes through the icon seam — the gate refused the first cut's bare <svg>, rightly); no PCT_TEXTS key, so not one other row of the snapshot moveddocs/acr/at/ — three readers, whole sandbox. Not reachable by this instrument: an avatar takes no focus, so a Tab walk never lands on one, and the logs of /avatar hold only the demo button (Swap the source · button.). That is consistent with the card claim that the component adds nothing to the tree, and it is not a test of it — a reader asked to READ the page would beapps/sandbox/src/app/views/avatar/ (the sandbox view; the dead-image demo breaks over a real 404 of this very server). The published site: /components/avatar — prerendered, the demo's own source is the code tablibs/components/avatar/src/avatar.spec.ts — 21 cases, the probe's five scripts as fixtures; libs/components/mutation.snapshot.md — avatar.ts at 87.50, 28 killed with zero clock-kills, and all four leftovers named: the reset arrow answering undefined for false (falsy either way, by construction), \s for \s+ (the word filter makes the two regexes one function), the segmenter's granularity option deleted (grapheme IS the platform's default — the option is documentation), and the return '' after the loop, a tail TypeScript demands and no non-empty word can reach. The first run also found real work: trim() beside filter(Boolean) was redundancy, deleted with a test that tells the two apartRead at build time from the tracked snapshots, not typed here.
What the component owns is the fallback chain — image, initials, silhouette, exactly one standing — and initials that are graphemes: the platform's segmenter, because charAt cuts an emoji family or a flag into a broken surrogate half. Both were measured in three engines before anything was written.
alt input on purpose — the name belongs to the text or the control beside the picture. A page with truly bare avatars names the control that holds them.