Avatar
Who a thing belongs to, at a glance — initials or a photo in a disc:
assignee columns, approval chains, bylines. Always decoration
(aria-hidden) with the name as real text nearby; it
scales with the text it sits beside.
Not as the only way to name someone — a disc of
initials in an Assignee column, with no name beside it, asks every
reader to decode "JK" and gives a screen-reader user nothing at all,
since the avatar is aria-hidden by contract. Pair it with
the name as real text — byline
does exactly that — or, in a dense table, drop the disc and keep the
name: the column is scanned and sorted on the text, never on the
picture.
Initials
View code
<span class="bo-avatar" aria-hidden="true">RV</span>
<span class="bo-avatar" aria-hidden="true">JK</span> A span with two letters. The disc is em-sized, so it follows the font size of its context — put it in a compact table and it shrinks with the rows, no modifier needed.
Photo
View code
<span class="bo-avatar" aria-hidden="true">
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%230f766e'/%3E%3Ccircle cx='16' cy='12' r='6' fill='%23ccfbf1'/%3E%3Cellipse cx='16' cy='27' rx='10' ry='8' fill='%23ccfbf1'/%3E%3C/svg%3E" alt="">
</span> An img fills the disc
(object-fit: cover). Keep alt="" — the
identity belongs in the adjacent text, not in the picture.
Approval-chain stack
View code
<!-- approval chain: DOM order = approval order -->
<div class="bo-avatar-stack" role="img" aria-label="Approvers: R. Vasquez, J. Kim, then M. Osei">
<span class="bo-avatar" aria-hidden="true">RV</span>
<span class="bo-avatar" aria-hidden="true">JK</span>
<span class="bo-avatar" aria-hidden="true">MO</span>
</div> The "who's next" strip: discs overlap
with a surface-colored ring, DOM order is approval order. The stack
itself carries the accessible name
(role="img" + aria-label listing the
chain) — one announcement, not three fragments.
In context — assignee column
| Invoice | Assignee |
|---|---|
| INV-10234 | |
| INV-10235 |
View code
<table class="bo-data-table" data-density="compact">
<thead><tr><th scope="col">Invoice</th><th scope="col">Assignee</th></tr></thead>
<tbody>
<tr><td>INV-10234</td><td><span class="bo-byline"><span class="bo-avatar bo-byline__avatar" aria-hidden="true">RV</span>R. Vasquez</span></td></tr>
<tr><td>INV-10235</td><td><span class="bo-byline"><span class="bo-avatar bo-byline__avatar" aria-hidden="true">JK</span>J. Kim</span></td></tr>
</tbody>
</table> Composed with
Byline:
the avatar takes both classes — .bo-avatar paints the
disc, .bo-byline__avatar tells the byline to lay out as
a flex row.
Markup
<!-- Decoration, not a caption: aria-hidden always, the NAME is real
text nearby (a byline, a cell, a stack's aria-label). em-sized —
it scales with the surrounding text, no size modifiers. -->
<span class="bo-avatar" aria-hidden="true">RV</span>
<!-- photo variant: img fills the disc; alt stays empty -->
<span class="bo-avatar" aria-hidden="true"><img src="…" alt=""></span>
<!-- inside a byline, compose both classes (byline's :has() keys on the
second one to switch to a flex row) -->
<span class="bo-byline">
<span class="bo-avatar bo-byline__avatar" aria-hidden="true">RV</span>
R. Vasquez · <time datetime="2026-08-16">today</time>
</span> Quick reference generated
| Class | Role |
|---|---|
| .bo-avatar | block (root) |
| .bo-avatar-stack | block (root) |
API generated from CSS
| Blocks | bo-avatarbo-avatar-stack |
|---|---|
| JS required | None — CSS-only. |
- Always
aria-hidden="true"— the avatar is decoration; the name is real text beside it (byline, cell) or on the stack (role="img"+aria-label). - No size modifiers by design: the disc is em-sized and inherits scale from its context (density, byline
--compact, headings). - Stack order is DOM order — put the current/next approver first and say so in the
aria-label. - Forced colors: the disc keeps a real border, so stacked avatars stay separable when backgrounds flatten.
- Promoted from Byline (owner call 2026-08-16): markup that carried only
.bo-byline__avatarmust add.bo-avatar— listed as Breaking in the CHANGELOG.
Contrast — 21 AA pairs this component uses (generated, CI-gated)
| Text | on | Light | Dark |
|---|---|---|---|
| text-primary | bg-surface | 17.74 | 16.15 |
| text-secondary | bg-surface | 10.31 | 11.46 |
| text-secondary | bg-muted | 9.37 | 9.75 |
| text-muted | bg-surface | 7.56 | 6.65 |
| text-muted | bg-muted | 6.87 | 5.66 |
| bg-surface | text-primary | 17.74 | 16.15 |
| danger-text | bg-surface | 6.47 | 8.89 |
| success-text | bg-surface | 5.02 | 12.02 |
| accent | bg-surface | 5.47 | 9.07 |
| text-secondary | bg-selected | 9.88 | 8.41 |
| text-secondary | bg-canvas | 9.86 | 12.82 |
| border-control | bg-surface | 4.83 | 6.65 |
| --bo-state-error-color | bg-surface | 4.83 | 6.1 |
| border-control | bg-muted | 4.39 | 5.66 |
| accent | bg-muted | 4.97 | 7.72 |
| warning-strong | bg-muted | 4.56 | 6.69 |
| warning-strong | bg-surface | 5.02 | 7.86 |
| danger | bg-muted | 4.39 | 5.19 |
| text-primary | bg-muted | 16.12 | 13.75 |
| success-text | bg-muted | 4.56 | 10.23 |
| danger-text | bg-muted | 5.88 | 7.57 |
Maturity generated
- Introduced
- v0.3.0
- CSS floor
- Chrome/Edge 105 · Firefox 121 · Safari 15.4
- Alignment scored
- 2026-08-23
- Assistive-tech evidence
- None recorded
Pin this component's first release with npm i @busy-office/ui@0.3.0.
The floor is derived from this component's stylesheet alone — set by
:has() — so it sits at or below the
whole framework's Chrome/Edge 119 · Firefox 129 · Safari 17.5.
No assistive-technology runtime result is recorded for any component: it needs a human listening to VoiceOver and NVDA on owner hardware; no gate in this repo can simulate announcement behaviour.
What automation covers instead is test:axe across every page at 1440 and 390, and check:forced-colors — see
accessibility.
Design-system alignment generated
100% (15/15) — how closely this component follows the design language. Each rule is scored 0-3 against a checkable citation; rules that don't apply leave the denominator. Scored 2026-08-23.
| Rule | Score | Evidence |
|---|---|---|
| Typography | 3 / 3 | 0.7em is geometry, not typography — two initials fitting a 1.8em disc, sized from the same host the disc is; stated in place (94.13) |
| Colour | 3 / 3 | zero raw colour; forced-colors adds the disc boundary that separates stacked avatars once backgrounds flatten |
| Spacing | 3 / 3 | the 2px stack ring states that it is a cut-out in the surface colour, not an outline; the -0.5em overlap is covered by the stack comment above it (94.6b) |
| Interaction | n/a | decoration; aria-hidden by contract |
| Content | 3 / 3 | opener carries the wrong-choice clause: "Not as the only way to name someone" — an initials disc with no adjacent name asks every reader to decode it and gives a screen-reader user nothing, since the avatar is aria-hidden by contract; points at byline, or dropping the disc in a dense table. Blind re-scored 2026-08-23 (Polish round 2), judged genuine: avatar-only assignee columns are a mainstream real pattern. |
| Fit | 3 / 3 | assignee columns, approval chains, bylines; em-sized so it inherits scale instead of shipping size modifiers |