Count bubble

Use the count bubble to say "something new needs you here": unread messages, open registrations, pending AI proposals. It sits on a nav item, a tab bar item or next to a heading.

Give it an accessible label that says what is counted ("3 offene Anmeldungen"). The visible number is hidden from screen readers and the label is read instead, so "Mitglieder 3" becomes "Mitglieder, 3 offene Anmeldungen".

With value: 0 nothing is rendered (unless showZero), so callers can always pass the current number. Large numbers are capped at max and shown as "99+".

Examples

Unread messages

3 ungelesen
Data
render({
  "value": 3,
  "label": "3 ungelesen"
})
Markup
<span class="count"><span aria-hidden="true">3</span><span class="u-visually-hidden">3 ungelesen</span></span>

Capped

128 offene Anmeldungen
Data
render({
  "value": 128,
  "label": "128 offene Anmeldungen"
})
Markup
<span class="count"><span aria-hidden="true">99+</span><span class="u-visually-hidden">128 offene Anmeldungen</span></span>

Without label (number is self-explanatory in context)

1
Data
render({
  "value": 1
})
Markup
<span class="count">1</span>

Data contract

What render(data) accepts — validated against this JSON Schema in development and tests. Unknown fields are rejected. The same schema is in components.json.

FieldTypeRequiredDefaultDescription
valueintegerrequired—Number of items.
≥ 0
labelstringoptional—Accessible text with number and noun, e.g. "3 ungelesen", "3 offene Anmeldungen". Read instead of the bare number. Strongly recommended.
min 1 chars
maxintegeroptional99Above this value the bubble shows "{max}+". The label should still contain the exact number.
≥ 9
showZerobooleanoptionalfalseRender the bubble for 0 as well (rarely useful; by default 0 renders nothing).
JSON Schema
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "value"
  ],
  "properties": {
    "value": {
      "type": "integer",
      "minimum": 0,
      "description": "Number of items."
    },
    "label": {
      "type": "string",
      "minLength": 1,
      "description": "Accessible text with number and noun, e.g. \"3 ungelesen\", \"3 offene Anmeldungen\". Read instead of the bare number. Strongly recommended."
    },
    "max": {
      "type": "integer",
      "minimum": 9,
      "default": 99,
      "description": "Above this value the bubble shows \"{max}+\". The `label` should still contain the exact number."
    },
    "showZero": {
      "type": "boolean",
      "default": false,
      "description": "Render the bubble for 0 as well (rarely useful; by default 0 renders nothing)."
    }
  }
}

Markup & states

Root: span.count — usable without render() by writing the markup directly.

Parts

PartDescription
span[aria-hidden="true"]The visible number (only when label is set).
.u-visually-hiddenThe accessible label, read instead of the number.

Accessibility

  • The number alone is meaningless to a screen reader ("Mitglieder 3"); always pass label unless the surrounding text already says what is counted.
  • The count bubble is not a live region. If counts change while the page is open, announce important changes elsewhere (e.g. a toast), not on every update.
  • Text is white on --color-danger (verified pair --color-on-status).

Guidance

DoCount only things the person can act on (open, unread, pending).
DoUse the same label wording as the destination page ("3 warten auf Freigabe").
Don'tDon't use a count for totals ("312 Mitglieder") — that's a stat tile.
Don'tDon't put a count on every nav item; it stops meaning "needs you".

Related: Status badge · App shell · Chip group