Skip to main content

Badge

betav0.1.0

Notification marker attached to another element: a small count, or a dot when the count itself does not matter. Replaces MUI `Badge` (27 render sites across OpenCTI and OpenAEV, all of them hand-styled through `sx`). Purely presentational and never interactive — no Radix primitive exists for a badge, and none is needed: there is no keyboard model, no state and nothing to focus. The dot is DERIVED from an absent `content`, and can also be FORCED while a count exists, for “something is waiting, do not show how much” — the accessible name stays complete either way. Boundary with `Chip`: a Chip labels (its own text carries the whole meaning and reads on its own line), a Badge marks (remove the element it sits on and it means nothing). The default tone is `"error"` (red) — changed from `"brand"` on 2026-08-13 because red is what products reach for most often; every other tone stays available and is chosen case by case.

Variants

light mode
before — tone="brand"4 unread, previous default, light
after — no tone passed4 unread, new default, light
before — tone="brand", dotUnread, previous default, light
after — no tone passed, dotUnread, new default, light
dark mode
before — tone="brand"4 unread, previous default, dark
after — no tone passed4 unread, new default, dark
before — tone="brand", dotUnread, previous default, dark
after — no tone passed, dotUnread, new default, dark
tone="brand" · count4 unread, brand
tone="error" · count4 unread, error
tone="success" · count4 unread, success
tone="neutral" · count4 unread, neutral
tone="brand" · dotUnread, brand
tone="error" · dotUnread, error
tone="success" · dotUnread, success
tone="neutral" · dotUnread, neutral
content={150} max={99}99+
content={150} · uncapped150
dot forced, count kept in the name12
content={0} · hidden
content={0} showZero0
invisible
on a real IconButton — count as description4
on a bare (tertiary) IconButton — 8px inset4
on a real IconButton — dot, circularAnchorUnread notifications
on a real button — forced dot, count still described12
standalone dotOperationalOperational
standalone countT105918

Examples

{/* No tone: the default is "error" (red) since 2026-08-13. */}
<Badge content={unreadCount} max={99}>
  <Icon name="bell" size={24} />
</Badge>
{/* The previous default, still available — now an explicit choice. */}
<Badge content={unreadCount} tone="brand" max={99}>
  <Icon name="bell" size={24} />
</Badge>
{/* Dot: no count to show. accessibleText is required — a bare coloured
    dot would carry its meaning by colour alone (WCAG 1.4.1). */}
<Badge tone="error" accessibleText={t('Unread notifications')}>
  <Icon name="bell" size={24} />
</Badge>
{/* Forced dot: there IS a count, but the exact number is not shown.
    It is still announced in full. */}
<Badge content={runningCount} dot tone="brand" circularAnchor>
  <IconButton icon={<Icon name="layers" />} aria-label={t('Bulk operations')} />
</Badge>
{/* Standalone: no children, no wrapper, no absolute positioning. */}
<Badge tone={healthy ? 'success' : 'error'} accessibleText={t(healthy ? 'Healthy' : 'Degraded')} />

Props

PropType
contentnumber | string — what the badge displays. Omit it to render the dot form. A number is capped by `max`; a string is never capped.
toneBadgeTone ("brand" | "error" | "success" | "neutral") — semantic colour. Default "error" (red) since 2026-08-13; it was "brand" before, so a badge rendered without this prop now paints red. The axis itself is unchanged: pass tone="brand" to keep the previous appearance.
dotboolean — force the dot rendering even though `content` is set (“something is waiting, do not show how much”). The value is still announced in full. Default false.
maxnumber — overflow cap for a numeric `content`: above it, renders `${max}+`. NO default (uncapped). A negative or non-finite value is treated as uncapped.
showZeroboolean — render the badge when `content` is 0. Default false.
invisibleboolean — do not render the badge at all (the node is not mounted, so nothing is left in the accessibility tree). Default false.
accessibleTextstring | false — what the ANCHOR is described as (the wrapper wires `aria-describedby` from the anchor to the badge). Omitted: the resolved value. A string: that string instead, and the visible digits are hidden from assistive technology. `false`: no description is wired and the badge is aria-hidden. REQUIRED when there is no `content`.
circularAnchorboolean — tuck the badge onto a round anchor instead of a square one (14% inset, MUI's own circular formula). Default false.
bareAnchor"md" | "sm" | false — declares that the anchor paints no resting surface: a tertiary IconButton, or any control styled through iconButtonVariants({ priority: "tertiary" }). At "md" the badge tucks 8px in on both axes so it stays visually attached to the glyph, which a filled or outlined anchor gets for free from its own painted box (Figma node 7611:19982). At "sm" the design file draws tertiary like the other treatments, so it resolves to the unchanged geometry — pass it anyway, so a later sm change lands in the library and not in your call site. Only "md" takes precedence over circularAnchor (the one combination where both ask for an inset); "sm" leaves circularAnchor in effect, because a round anchor still needs its tuck whether or not it paints a resting surface. Default false.
childrenReactNode — the element the badge marks. Omit it to render the badge standalone, in normal flow, with no wrapper and no absolute positioning.

Accessibility

2.1 AAWCAG pass

The badge DESCRIBES its anchor. When `children` is given, the wrapper clones the single child and gives it `aria-describedby` pointing at the badge element, so the anchor's own accessible NAME is untouched and the count is its accessible DESCRIPTION — proven by `toHaveAccessibleName`/`toHaveAccessibleDescription` on a real <button>, not by reading the DOM. This deliberately replaces relying on the accname algorithm sweeping the badge's text out of the anchor's subtree: that only works for an anchor named by its contents, and 7 of the 27 inventory sites put the badge inside an `IconButton` with an `aria-label`, where the label wins and the count would have been announced by nobody. `accessibleText` replaces the announced string and HIDES the visible digits from assistive technology, so the description is the consumer's phrase alone and never both. `accessibleText={false}` takes the badge out of the accessibility tree AND adds no `aria-describedby`, for a consumer who already named the anchor. An anchor that already had an `aria-describedby` keeps it — the badge appends. No English phrase is ever synthesised: a bilingual product must not be handed a hardcoded English string. If `children` is not a single React element there is nothing to clone onto: the badge still renders, the description is not wired, and a dev-only `console.warn` says so (AGENTS.md "Prop contract violations"). A dot with no `accessibleText` is a TYPE ERROR, and the same dev warning catches the untyped call sites the compiler never sees, because a bare coloured dot carries its meaning by colour alone (1.4.1). Forcing the dot while `content` is set is exempt: the value is still described, so the name never degrades with the visual. No live region: 12 of the 27 inventory sites are polling counters that would chatter, and 12 are dots with nothing to announce; a consumer who needs one passes `aria-live` through, so the default is proposed and never imposed. No disabled state: no badge in either product is disabled (MUI's Badge has no such prop), and a marker that carries information is not something one deactivates. Never interactive: no `onClick`, no `role`, no `tabIndex`, no focus ring — a clickable counter is the consumer's own <button> wrapped by this component. Default tone: moving it from `brand` to `error` on 2026-08-13 added NO pair and removed none — both tones were already declared and measured above, and the change only picks a different one of the four. It is a visual change, not an accessibility one: the error counter (8.81:1 dark / 6.27:1 light) and the error dot (5.18:1 / 5.92:1 against the reference surface) both clear their thresholds by a wider margin than the brand pair they replace as the default. No `alert` tone was added: the `feedback-alert` family is amber (--yellow-400, the same value in both modes), and its dot measures 1.54:1 against every light surface, below WCAG 1.4.11's 3:1 floor — it could not have shipped.