ComponentsData & statusAvatar
On this page
Data & status

Avatar released

The picture beside a name, with initials and then a silhouette standing in when there is no picture.

  • Import@pacit/components/avatar
  • Selectorpct-avatar

No APG pattern applies, and none is invented — the reasoning is under Accessibility ↓

avatar.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctAvatar } from '@pacit/components/avatar';
ExportedWhat it is
PctAvatarcomponentpct-avatarAn avatar: the picture beside a name — and never the name itself.
<pct-avatar name="Ada Lovelace" />
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

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.

API

What it reads, and what it writes back

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.

Inputs

NameTypeWhat it does
namedefault ''stringThe 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 ''stringThe picture's URL. Empty means none — the initials (or the silhouette) stand instead.
sizedefault config.defaultSizePctSizeSize on the shared control axis; from the global configuration by default.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-avatar
aria-hiddentrueDecoration 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-sizesize()
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

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.

PartSelectorWhat 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

Tokens

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.

TokenStyles
--pct-avatar-bg{pct.surface-100}#f1f5f9 · #1e293bBackground of the avatar.
--pct-avatar-border{pct.border}#e2e8f0 · #334155Edge colour of the avatar.
--pct-avatar-fg{pct.text}#0f172a · #ffffffText colour of the initials.
--pct-avatar-font-size{pct.font.size.md}14pxFont size of the avatar.
--pct-avatar-font-size-lg{pct.font.size.lg}16pxFont size of the avatar at size lg.
--pct-avatar-font-size-sm{pct.font.size.sm}13pxFont size of the avatar at size sm.
--pct-avatar-icon-fg{pct.text-muted}#334155 · #e2e8f0Text colour of the icon.
--pct-avatar-radius999pxCorner radius of the avatar.
--pct-avatar-size{pct.control.height.md}36pxSize of the avatar.
--pct-avatar-size-lg{pct.control.height.lg}44pxSize of the avatar at size lg.
--pct-avatar-size-sm{pct.control.height.sm}28pxSize of the avatar at size sm.

Theming

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;
}
Accessibility

Measured, not assumed

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.

Keyboard

keyeffecttest

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).

What is measured, and what is not yet

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.

MeasuredARIA pattern named in the class JSDoclibs/components/avatar/src/avatar.ts
MeasuredKeyboard map tested key by keyapps/sandbox-e2e/src/avatar.spec.ts — the empty map is the claim: no element in the component takes focus in any of the three engines
Measuredaxe audit on the component's own sandbox viewapps/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 on
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — avatar-chain and avatar-chain-rtl: all three links of the chain in one frame
Measuredforced-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.ts
n/aprefers-reduced-motion — duration from a tokennot applicable — the component has no motion at all: no transition, no loop, so the preference has nothing to slow
n/aTouch target ≥ 24×24 px outrightnot applicable — nothing here is a target. An avatar takes no pointer and no focus, so the floor req-a11y-touch sets has nothing to apply to
MeasuredSize axis aligned 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)
GapDensity axisnone — gap: the same one every component here has (req-token-density)
MeasuredRTL — no physical properties + a 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-rtl
MeasuredSSR + hydration with no NG05xxapps/sandbox-e2e/src/hydration.spec.ts — /avatar in SBX_ROUTES
n/aFormsnot applicable — a decoration holds no value a form owns and implements no FormValueControl
MeasuredParts registered in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs — the Parts row above
MeasuredTokens registered + an entry in contrast.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.md
n/aStrings through PCT_TEXTSnot applicable — the component draws no text of its own; the initials are the consumer's name, redrawn
MeasuredEntrypoint size budgetlibs/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 moved
MeasuredScreen-reader logRead 2026-09-16 from docs/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 be
MeasuredA docs page with live examplesapps/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 tab
MeasuredUnit + mutationlibs/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 apart
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

87.5%mutation score28 of 32 mutants killed
8 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
3colour pairs measuredlight and dark, at every token build
2visual baselinesChromium, the sandbox's own pictures
6elements in the preview0 listeners · 1 render to settle · 2959 µs in jsdom, held exactly but for the clock

Why it is built this way

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.

Known limitations

  • It never speaks, and cannot be made to. There is no 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.
  • No status dot. Presence is a fact with its own text; a coloured dot as its only channel fails forced colours and screen readers at once. Where status exists it is the page's sentence.
  • No group/stack. Overflow counts, overlap order and one label for many are a different component's questions; refusing them keeps this one a leaf.
  • No loading state. The box is token-sized either way, so nothing moves when the image lands; the initials already stand where the picture is late, and they are a better placeholder for a face than motion is.
  • The initials follow words, not scripts' name orders. First grapheme of first and last whitespace-separated word — a CJK full name written without spaces yields one grapheme, which is also what those scripts' own conventions draw.