ReviewOS

stacks/stx

Badge sets `role="status"` unconditionally, making every badge an ARIA live region

#2044
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Badge puts role="status" on its root, so every badge on a page is an ARIA live region.

src/ui/badge/Badge.stx renders unconditionally:

<span class="" role="status">

role="status" is an implicit aria-live="polite" region. A badge is almost always a static label on a row or a card, not a status message, and making each one a live region has two consequences:

  1. Any text change inside a badge is announced, even when the user's attention is elsewhere. A list that re-renders announces every badge in it.
  2. Live regions are meant to be few and deliberate. One page in our estate (statushq) has 126 badge-shaped elements; if those were all <Badge>, the page would carry 126 polite live regions. Screen readers do not degrade gracefully here — the queue becomes unusable noise.

A badge that genuinely reports changing status does exist, which is why the role should be opt-in rather than removed outright.

Suggested fix

Add a live (or announce) prop, default false, and emit role="status" only when set:

<span class="" >

Callers that want the announcement ask for it; the ordinary labelling case stays silent.

Found on @stacksjs/components 0.2.382. Context: we are adopting this family across six apps and Badge is otherwise ready — it is fully role-token based, 0 hard-coded palette values, and it already went through the #1993 token pass.

9 days ago

Fixed on 0.2.395 — verified by rendering. <Badge variant="success">Up</Badge> emits no role, and <Badge … live> opts into role="status". That is exactly the shape that was needed: the labelling case is silent and a badge that genuinely reports change can still ask to be announced. Thanks.

9 days ago

Fixed in ae7343a, shipped in v0.2.385 — implemented as suggested here.

role="status" is now behind an opt-in prop:

<span class="" >
  • <Badge>New</Badge> → no role, no aria-live
  • <Badge live>7</Badge> → role="status" aria-live="polite"
  • <Badge live="assertive">7</Badge> → aria-live="assertive"

One addition beyond the suggestion: live is read with $bool, so live="false" is off. Without that, the attribute spelling would turn a badge into a live region when asked for the opposite — the failure mode from #2006, and it would have been a nasty one to hit here precisely because the symptom is silence that only a screen-reader user notices.

aria-live is emitted alongside role="status" rather than relying on its implicit value, so the polite/assertive distinction is expressible at all.

The reasoning in this report is what made it straightforward — particularly keeping the role rather than removing it, since a badge that genuinely reports changing status is a real case. Covered by packages/components/test/badge-tooltip-announcement.test.ts, including the live="false" case.

Sign in to comment on this issue.