Skip to content

fix(avatar): pair each status-dot variant with a distinct shape#4157

Merged
humbertovirtudes merged 2 commits into
facebook:mainfrom
AKnassa:ak-4143-statusdot-a11y
Jul 24, 2026
Merged

fix(avatar): pair each status-dot variant with a distinct shape#4157
humbertovirtudes merged 2 commits into
facebook:mainfrom
AKnassa:ak-4143-statusdot-a11y

Conversation

@AKnassa

@AKnassa AKnassa commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #4143.

What this does

Avatar status dots used to differ only by colour — green for online, grey for away, red for busy. For colour-blind users those can look identical. Now each status also has its own shape:

  • success - filled dot (unchanged)
  • neutral - ring
  • error - dot with a minus bar

The same shapes Slack, Teams, and Discord use, so people already know what they mean.

Why

WCAG 2.1 Level A (SC 1.4.1) says colour can't be the only way information is conveyed. The issue's colour-vision simulations show green and red collapsing into the same olive. With shapes, the status reads even with no colour perception at all.

What changed

  • Each variant draws its shape at every size — including the smallest avatars, where icons can't fit.
  • A custom icon still takes over as the visual mark when it renders (docs now say to use a different icon per status).
  • The shape is a stable theming target: astryx-avatar-status-dot-glyph + data-shape.
  • First tests for this component (14, geometry pinned exactly), full suite and typechecks green.
  • Docs now note honestly that the dot's label isn't announced inside an Avatar today (the Avatar root is role="img") — composing it into the avatar's name is a planned follow-up.
  • Two small pre-existing doc bugs fixed: an error dot labelled "Online" in a CLI block, and a wrong slot element in the Avatar docs.
all-sizes-dark all-sizes-light small-sizes-achromatopsia small-sizes-dark small-sizes-deuteranopia small-sizes-light small-sizes-protanopia with-icon-dark with-icon-light with-status-achromatopsia with-status-dark with-status-deuteranopia with-status-light with-status-protanopia

How to see it

Storybook → Core/Avatar → Status Shapes at Small Sizes (new story). Screenshots attached below, including colour-blindness simulations, light and dark.

Kept as draft on purpose

The ring/minus shapes change how shipped neutral and error dots look (the default success is unchanged). That visual vocabulary deserves a maintainer's okay before this goes ready.

Note: the standalone StatusDot component has the same colour-only pattern across five variants — a follow-up issue for it is being filed separately.

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 24, 2026 1:13am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 22, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:design-review Affects visuals — Design should review labels Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

Modified Components

Avatar
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 433 -
Complexity N/A Very High (40) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.7KB 0B

Accessibility Audit

Status: 1 accessibility violation(s) found — 1 serious.

Avatar - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 2/13 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Generated by PR Enrichment workflow | View full report

@AKnassa
AKnassa marked this pull request as ready for review July 22, 2026 02:30
@ernestt

ernestt commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Thanks for tackling this — the accessibility direction is right, and we do want to support shape-differentiated status dots (not colour alone). Approving the intent; one thing to refine before it lands.

The glyph weight feels off against the rest of the system. Right now the ring band and minus bar are ~25% of the glyph's inner field at every tier (2px on an 8px field, 4px on 16px, 6px on 24px). That's heavier than any other glyph we ship, which is why they read as a bit clunky rather than "system-native."

For comparison, our existing inline SVG glyphs are much lighter and use stroked paths with round caps:

  • Icon default set (packages/core/src/Icon/defaultIcons.tsx) — strokeWidth: 1.5 in a 24 viewBox (~6%), menu at 2 (~8%).
  • CheckboxInput checkmark — a stroked <path> with strokeWidth="1.5" and strokeLinecap="round"/strokeLinejoin="round".

Two suggestions to make the glyphs feel cohesive:

  1. Target ~2px stroke in a 24 viewBox and scale proportionally (roughly stroke = glyph / 12), capping at a 1px minimum for the smallest tier. That puts these in line with the icon family (~6–8%) instead of at 25%.
  2. Render them as tiny inline <svg> glyphs rather than CSS box <span>s — same zero-dependency approach we already use for the checkbox mark and defaultIcons, but it buys us sub-pixel stroke control and round line caps that a CSS box can't give. A stroked ring and a rounded minus bar will look intentional at every size, including the 10px tier.

Net: keep the shape vocabulary (filled / ring / minus) exactly as you have it — just lighten the stroke and move to stroked SVG so it matches how the rest of the system draws glyphs.

AKnassa added 2 commits July 23, 2026 20:11
AvatarStatusDot conveyed status by colour alone, failing WCAG 2.1
Level A SC 1.4.1 (Use of Color). Each variant now pairs its colour
with a built-in shape: success stays the filled dot, neutral renders
a ring, error a minus bar — surface-coloured cutouts that stay
legible at every size tier, including the smallest where icons
cannot render. A rendered icon (isRenderable, not truthiness)
replaces the glyph; custom augmented variants are documented as
needing their own non-colour mark. The glyph is a stable theming
target: astryx-avatar-status-dot-glyph + data-shape.

Also fixes two pre-existing doc bugs surfaced by review: an error
dot labelled Online in the AvatarShowcase CLI block, and a wrong
slot element (StatusDot / invalid variant) in Avatar.doc.mjs.

Fixes facebook#4143
…h weight (facebook#4143)

Review feedback: the ring band and minus bar were ~25% of the dot's inner
field at every tier, several times heavier than any other glyph we ship.

Draw them as stroked inline SVG instead of CSS box cutouts — the same
zero-dependency approach as Icon's defaultIcons and the CheckboxInput
checkmark — which buys sub-pixel stroke control and round line caps that a
box cannot give at a 10px dot. Stroke is now roughly field/12 floored at
1px, landing on the system's 1 / 1.5 / 2 ladder.

A thin stroke only reads as a ring if the interior is not the variant
colour, so `neutral` inverts to a surface plate with a coloured stroke.
Everything drawn on the dot — glyph and any user `icon` — now paints from
the dot's `currentColor`, so an icon on the ring variant stays legible
instead of rendering surface-on-surface.

Shape vocabulary (filled / ring / minus) is unchanged. Verified at true 1x
under deuteranopia, protanopia and achromatopsia: all three shapes still
separate without colour at the smallest tier.
@AKnassa

AKnassa commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. I checked all three of your points against the code before changing anything, and they all hold up:

  • The ring band and minus bar really were 25% of the inner field at every tier (2px on 8, 4px on 16, 6px on 24).
  • Icon's default set is strokeWidth: 1.5 in a 24 viewBox, with menu at 2.
  • The CheckboxInput checkmark is a stroked path at 1.5 with round caps and joins.

So I have done both things you asked. The glyphs are now stroked inline SVG (a <circle> for the ring, a round-capped <line> for the minus bar) rather than CSS box cutouts, and the stroke is a lot lighter.

Stroke ladder. I went with 1 / 1.5 / 2px across the three tiers, whose inner fields are 8, 16 and 24px. That is your field / 12 with the 1px floor at the small tier, except at medium, where the formula gives 1.33 and I rounded to 1.5 so it sits exactly on the stroke the rest of the system draws with. Small and large match your formula exactly. If you would rather I keep the strict 1.33 for proportionality, I am happy to change it.

One knock-on effect worth flagging. A thin stroke only reads as a ring if the interior is not the variant colour, otherwise it reads as a bullseye. So neutral had to invert: the plate is now surface and the colour moved to the stroke. On its own that would have made a user-supplied icon on a neutral dot invisible, because the icon wrapper hard-coded surface ink. Everything drawn on the dot now paints from the dot's currentColor, both the glyph and any icon, and each variant sets its fill and its ink together. There is a test pinning that so it cannot quietly regress.

The accessibility check. Since the point of this PR is that shape carries the status, I did not want to take a lighter stroke on trust. I rendered old and new at true 1x, magnified nearest-neighbour so I was looking at real device pixels, under deuteranopia, protanopia and achromatopsia, in light and dark. All three shapes still separate with no colour at all, including at the smallest tier. The ring actually reads better than it did: at 10px the old thick band looked like a filled blob with a pinhole, and the new one is unmistakably an outline.

One honest caveat. A 1px stroke cannot pixel-centre in an 8px field, so at 1x the smallest minus bar anti-aliases slightly softer than the old 2px bar. It is exactly 2 device pixels and crisp at 2x. That is the direct trade for the sub-pixel control, and it still reads clearly in the achromatopsia renders, but I would rather name it than hide it.

I also rebased past #4140, so the size scale rename is picked up and the branch is mergeable again.

The shape vocabulary is unchanged, as you asked. Note that the vocabulary itself (ring for neutral, minus for error) is still waiting on a call from @cixzhang, since it changes how shipped neutral and error dots look.

@gonzoblasco

Copy link
Copy Markdown

Hey @AKnassa — 👋

I'm the author of #4218, which tackles the same WCAG 1.4.1 issue on AvatarStatusDot from the icon+label angle. @humbertovirtudes reviewed it and suggested we converge, pointing to your PR as the stronger base (and I agree — the SVG shapes work at every size, which icons can't do at the smallest tier).

What I'd like to propose:

Your shapes approach is solid. What's missing (and what my PR adds) is a default accessible label per variant, so the status is never conveyed by colour alone even when shapes aren't visible or the dot is inside a Avatar root.

Specifically:

  • A map: success → "Online", neutral → "Away", error → "Busy" (per humbertovirtudes's suggestion — presence naming reads better than "Success"/"Neutral"/"Error" for a status dot)
  • The label resolves from: explicit label prop → default per variant → none
  • The role="img" + aria-label is always set when a label is available, so AT always gets the status meaning

I'm happy to close #4218 and fold this label piece into your PR if you're open to it. Want me to open a PR against your branch, or would you rather cherry-pick the relevant parts?

Also worth noting: your PR already documents that the label isn't announced inside Avatar today (because Avatar root is role="img"). The default label won't fully solve that until the Avatar composition story is figured out, but it's still a net improvement — at least the dot has a label when inspected directly or when the Avatar structure changes later.

Let me know what works best for you!

@humbertovirtudes humbertovirtudes left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lovely fix. Pairing each variant with a distinct shape topology (filled dot, ring, minus bar) so status never rides on color alone is exactly the resolution the component needed, and drawing the glyph as a stroked inline SVG in {currentColor} is what makes it hold up at the 10px tier where a CSS box cutout can only land on whole pixels.

Highlights:

  • The {currentColor} discipline: painting both the glyph and any user icon from the same ink so plate and mark can never drift out of contrast, and inverting the neutral variant to a surface plate so a hollow ring actually reads as hollow.
  • Honesty about the boundary: documenting that a label on the dot is not announced inside Avatar's role="img" and flagging it as a planned Avatar-level follow-up, rather than ignoring it.
  • A rendered icon suppressing the built-in glyph (two cutouts in one tiny field would make both illegible) is the right precedence.
  • Semantic tokens preserved throughout, and a stable astryx-avatar-status-dot-glyph class plus data-shape so themes have a real hook.

One non-blocking note: the GLYPH_STROKE_WIDTH ladder comment is on the long side. The math is non-obvious so some of it earns its place, but it could tighten a touch. Not worth holding the PR tho.

nice work.

@github-actions github-actions Bot removed the needs:code-review High-risk change (new package/component/API) — needs human code review before merge label Jul 24, 2026
@humbertovirtudes
humbertovirtudes merged commit 433a57e into facebook:main Jul 24, 2026
21 checks passed
@humbertovirtudes

humbertovirtudes commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Hey @AKnassa — 👋

I'm the author of #4218, which tackles the same WCAG 1.4.1 issue on AvatarStatusDot from the icon+label angle. @humbertovirtudes reviewed it and suggested we converge, pointing to your PR as the stronger base (and I agree — the SVG shapes work at every size, which icons can't do at the smallest tier).

What I'd like to propose:

Your shapes approach is solid. What's missing (and what my PR adds) is a default accessible label per variant, so the status is never conveyed by colour alone even when shapes aren't visible or the dot is inside a Avatar root.

Specifically:

  • A map: success → "Online", neutral → "Away", error → "Busy" (per humbertovirtudes's suggestion — presence naming reads better than "Success"/"Neutral"/"Error" for a status dot)
  • The label resolves from: explicit label prop → default per variant → none
  • The role="img" + aria-label is always set when a label is available, so AT always gets the status meaning

I'm happy to close #4218 and fold this label piece into your PR if you're open to it. Want me to open a PR against your branch, or would you rather cherry-pick the relevant parts?

Also worth noting: your PR already documents that the label isn't announced inside Avatar today (because Avatar root is role="img"). The default label won't fully solve that until the Avatar composition story is figured out, but it's still a net improvement — at least the dot has a label when inspected directly or when the Avatar structure changes later.

Let me know what works best for you!

Yall can follow up on a separate PR now that this one is merged :P

Keeping changes compact makes for easier reviews

Thanks for following up!

@gonzoblasco

Copy link
Copy Markdown

Followed up on a separate PR as suggested — #4298 adds the default accessible labels ("Online"/"Away"/"Busy") on top of the shapes base that landed here.

@humbertovirtudes would you mind taking a look when you have a moment? Added you as a reviewer.

@github-actions
github-actions Bot deleted the ak-4143-statusdot-a11y branch July 25, 2026 08:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] WCAG 2.1 A 1.4.1 fail - use of colour failure, accessibility

4 participants