Avatar — accessibility

Keyboard

  • Not focusable and not interactive. When it sits in a chip or a row, that host is the control.
  • One exception, and it is the host again: on the person card the mark is wrapped in button.tbar-mark.round, which opens the picture dialog. The button carries the name and the focus ring; the mark inside it stays inert.

ARIA

  • Without a picture the mark is aria-hidden="true": the initials are a compressed rendering of a name that is already in the DOM next to it, and reading "JN" before "Josef Novák" is noise.
  • With a picture it is not hidden, and the <img> carries alt="" — a photograph of a person whose name is already written beside the mark adds nothing a screen reader can use, so it is decorative for the same reason.
  • title is set to the full name, which surfaces on hover for a sighted mouse user; it is not a substitute for the name being present as text.
  • Consequence: an avatar must be accompanied by the name in text. On its own it is invisible to a screen reader, by design.

Focus

  • Nothing focusable, so no focus ring. The chip or row around it keeps the foundation ring from ds.css.
  • Where the mark is the upload trigger, the ring belongs to the button and follows its shape: round for a person's photo, --radius-md for a company logo.

Contrast

Computed from packages/ui/src/tokens/tokens.css:

  • Ground is --surface-75 in both themes; the ring and initials are hsl(var(--av-hue) …), so the exact ratio varies with the hash. The palette is tuned to stay legible, but a specific hue is not guaranteed to clear AA for 11px text.
  • Because the mark is aria-hidden and always accompanied by the name, the initials are not load-bearing text — the name beside it is.
  • In the picture case the ring is --border, a hairline that separates the picture from the page. It is decoration, not information: it is never the only thing telling a person or a company apart.
  • A photograph carries no contrast guarantee at all — whatever was uploaded is what shows. Nothing is written over it, so nothing can become illegible.

Known limits

  • Hue comes from a hash, so two different people can land on the same colour; the colour is a memory aid, never an identifier.
  • The colour is not announced and cannot be perceived by a user who cannot distinguish the hues — nothing is lost, because the colour carries no information the name does not.
  • Initial contrast depends on the hashed hue and is not individually verified per hue.
  • An avatar used without an accompanying name is silent to assistive technology.
  • The bare primitive has no onError: a picture that fails to load, or that the viewer may not read, leaves the browser's broken-image glyph. Only the app wrappers (PersonPhoto, OrgLogo) recover, falling back to the initials — which is why screens must not call <Avatar photo> directly.
  • A picture is personal data. It is served same-origin through the API under the record's own read permission (404, never a public URL), and no attribute is derived from it — no face recognition, no matching, no deduplication.

Source: docs/avatar/avatar.a11y.md · GitLab