v1.0.0

<bmx-badge>

A count or a state, attached to something else.

8 properties · 0 events · 0 methods · 2 parts

Example

The bubble on the left draws 99+; a screen reader is told “Inbox, 127 unread messages”. 99+ is a truncation the layout asked for, and nobody listening to the page has that constraint — so the drawn text and the announced text are two different strings.
A count of zero draws nothing, because a badge that appears when nothing needs attention teaches the reader to stop looking at it. show-zero is there for the scoreboard case.
To badge a control, use for rather than wrapping it. A description has to attach to the element that takes focus, not to a box drawn round it — so a badge that encloses a button cannot describe it. The slot is for a picture: an avatar, an icon, anything the user cannot focus. Put a focusable control in it and the component says so once in the console.
Show markup
<div class="row" style="gap: 2.5rem; align-items: center">
  <span style="position: relative; display: inline-flex">
    <button id="ex-inbox" type="button" style="padding: 0.5rem 0.9rem">Inbox</button>
    <bmx-badge for="ex-inbox" value="127" label="unread messages" tone="danger"
      style="position: absolute; inset-block-start: 0; inset-inline-end: 0; transform: translate(35%, -35%)"></bmx-badge>
  </span>

  <bmx-badge value="Beta" tone="info"></bmx-badge>
  <bmx-badge value="Renewed" tone="success"></bmx-badge>
  <bmx-badge value="Action needed" tone="warning"></bmx-badge>
  <bmx-badge dot tone="success" label="Online"></bmx-badge>
</div>

<div class="row" style="margin-block-start: 1.5rem">
  <span class="note">
    The bubble on the left draws <strong>99+</strong>; a screen reader is told <em>&ldquo;Inbox, 127 unread
    messages&rdquo;</em>. <code>99+</code> is a truncation the layout asked for, and nobody listening to the page has
    that constraint &mdash; so the drawn text and the announced text are two different strings.
  </span>
</div>

<div class="row" style="margin-block-start: 1rem; gap: 1.5rem; align-items: center">
  <bmx-badge value="3">
    <bmx-avatar name="Amara Okonkwo" decorative></bmx-avatar>
  </bmx-badge>

  <bmx-badge value="0" label="unread">
    <bmx-avatar name="Grace Hopper" decorative></bmx-avatar>
  </bmx-badge>

  <span class="note" style="flex: 1 1 18rem">
    A count of zero draws nothing, because a badge that appears when nothing needs attention teaches the reader to stop
    looking at it. <code>show-zero</code> is there for the scoreboard case.
  </span>
</div>

<div class="row" style="margin-block-start: 1rem">
  <span class="note">
    <strong>To badge a control, use <code>for</code> rather than wrapping it.</strong> A description has to attach to
    the element that takes focus, not to a box drawn round it &mdash; so a badge that encloses a button cannot describe
    it. The slot is for a picture: an avatar, an icon, anything the user cannot focus. Put a focusable control in it and
    the component says so once in the console.
  </span>
</div>
<!-- a status, on its own -->
<bmx-badge value="Beta" tone="info"></bmx-badge>

<!-- a count, on a control the user can focus -->
<button id="inbox">Inbox</button>
<bmx-badge for="inbox" value="127" label="unread messages"></bmx-badge>

<!-- a count, over a picture -->
<bmx-badge value="3" label="new photographs">
  <bmx-avatar name="Ada Lovelace" decorative></bmx-avatar>
</bmx-badge>

THE TWO THINGS THIS COMPONENT HAS TO GET RIGHT

One: the count has to reach the control, not float beside it. A red bubble on the corner of a bell tells a sighted user there are three notifications because it is on the bell. Proximity is the whole message, and proximity is exactly what does not survive into an accessibility tree: a screen reader meets a button called "Inbox" and then, separately, the number 127, with nothing to say the two are related. So a badge on a focusable control is attached with for, which writes aria-describedby on it, and the control is then announced as "Inbox, 127 unread messages".

Two: 99+ is a truncation and is never spoken. It exists because the bubble has room for two digits. Nobody listening to the page has that constraint. The drawn text and the announced text are two different strings for this reason; see src/core/badge.ts.

WHY for RATHER THAN WRAPPING THE BUTTON

Wrapping is what the visual design looks like, so it is the first thing anybody reaches for - and a description has to be attached to the element that takes focus, which is the button inside, not the box drawn round it. A badge that wraps a button therefore cannot describe it without reaching into markup it does not own. for puts the reference on the right element and leaves the positioning to CSS, where it belongs.

The slot is still there and is right for a picture: an avatar with a presence dot, an icon with a count, anything the user cannot focus. If a focusable element is slotted in without a for, the component says so once in the console - it is the same class of mistake as an X on a chip that is not a button, and it is invisible until somebody tries the page without a mouse.

Properties

PropertyAttributeTypeDefaultDescription
dot dot boolean false Draw a mark with no number in it. Then label is not optional: a dot with nothing to say is a decoration, and this component makes it one - aria-hidden, out of the tree entirely.
for for string The id of the control this badge belongs to. The badge writes aria-describedby on it, so the count is announced as part of that control rather than as a loose number beside it. The control must be in the same document or shadow root as the badge, which is what an ID reference means everywhere in the platform.
label label string What is being counted: unread messages, items in your basket. It reaches the announcement and never the bubble. Without it a screen reader is given a bare number, which is the accessible equivalent of a red dot with no explanation.
max max number 99 The largest count drawn in full. Past it the bubble shows 99+, and the exact number is still announced.
placement placement BmxBadgePlacement 'top-end' Where the bubble sits over slotted content.
showZero show-zero boolean false Draw a count of zero instead of nothing.
tone tone BmxBadgeTone 'neutral' What the badge is reporting. Meaning, not colour: the colours are custom properties.
value value number | string The count, or a short word. A string of digits is treated as the count it is, because value="3" from a Razor page or an htmx swap is the commonest way this is ever set.

Slots

SlotDescription
(default) Something for the bubble to sit on. Not a focusable control - see above.

CSS shadow parts

PartDescription
anchor The box the slotted content sits in.
badge The bubble.

CSS custom properties

PropertyDescription
--bmx-badge-background The bubble's fill, overriding the tone.
--bmx-badge-border The ring that separates the bubble from what it sits on.
--bmx-badge-color The text on it.
--bmx-badge-dot-size The diameter of the dot form.
--bmx-badge-font-size The digits.
--bmx-badge-font-weight How heavy they are.
--bmx-badge-height The bubble's height, and its diameter when it holds one digit.
--bmx-badge-offset How far the bubble hangs off the corner of slotted content.
--bmx-badge-padding-inline The room either side of a longer count or a word.
--bmx-badge-radius The corner radius. A pill by default.
--bmx-badge-tone-danger The fill for tone="danger".
--bmx-badge-tone-info The fill for tone="info".
--bmx-badge-tone-neutral The fill for tone="neutral".
--bmx-badge-tone-success The fill for tone="success".
--bmx-badge-tone-warning The fill for tone="warning".
--bmx-badge-tone-warning-text The text on the warning fill, which is dark rather than white.