Badges
Summary
Badges are read-only status pills and count
bubbles: tinted semantic variants, a bold "must not miss" fill,
the solid -primary count bubble, and overlay/dot
forms that pin to an icon button's corner (approvals counts,
unread dots). User-managed tags are
Labels; toggleable filter pills are
Chips.
When to use
| Component | Use it for |
|---|---|
| Badge (tinted) | Status at a glance in tables and lists — healthy, degraded, failed. Never clickable. |
Badge -primary | Counts: notifications, pending approvals. Solid fill so it reads at bubble size. |
| Badge overlay / dot | Pinning a count or presence marker to an icon button's corner. |
| Label | Folksonomy the user edits: tags on issues, topics on projects. |
| Chip | Filter toggles above tables and lists (KEV-listed, Suppressed, …). |
Examples
Overlays & dots
.soma-badge-overlay goes on the host (any
positioned inline element, typically a
.soma-navbar-action icon button) and pins the child
badge to its top inline-end corner (mirrored under RTL). The
overlaid bubble is slightly smaller than a standalone badge so it
doesn't swallow the icon. .soma-badge-dot is the
8px presence form for "there is something" without a number —
unread indicators, attention markers. When the state clears,
remove (or hide) the badge element; the host keeps its footprint.
Counts and screen readers
The host's aria-label carries the count.
An overlaid bubble reading "3" is meaningless out of context to a
screen reader ("button, three"), and a dot says nothing at all,
so the badge is treated as decoration and the host button owns the
full announcement: aria-label="Approvals, 3 pending",
aria-label="Notifications, unread". Whenever the
count changes, update the badge text and the
aria-label together: one function, both writes (see
the live example above). Standalone text badges in tables
("failed", "active") need nothing extra: their text is their
announcement.
HTML
A status pill; the text is the announcement, no ARIA needed:
<span class="soma-badge soma-badge-success">healthy</span>
A count bubble next to its subject (e.g. in a tab or list row):
Queue <span class="soma-badge soma-badge-primary">7</span>
The bold fill for states that must not be missed:
<span class="soma-badge soma-badge-bold soma-badge-danger">FAILED</span>
A count pinned to an icon button — -overlay on the
host, the number in its aria-label:
<button class="soma-navbar-action soma-badge-overlay" aria-label="Approvals, 3 pending">
<span class="soma-icon soma-icon-check"></span>
<span class="soma-badge soma-badge-primary">3</span>
</button>
A presence dot with no number, so the meaning lives entirely in the host's label:
<button class="soma-navbar-action soma-badge-overlay" aria-label="Notifications, unread">
<span class="soma-icon soma-icon-bell"></span>
<span class="soma-badge soma-badge-dot"></span>
</button>
Inside a solid button the badge flips to translucent white automatically:
<button class="soma-button soma-button-primary">Approvals
<span class="soma-badge">3</span></button>
CSS classes
| Class | Effect |
|---|---|
.soma-badge | Base pill (neutral tint). |
.soma-badge-primary | Solid blue count bubble. |
.soma-badge-success / -warning / -danger / -info | Tinted semantic variants. |
.soma-badge-bold | Combines with the semantic variants for a solid "must not miss" fill (bare -bold is solid neutral). |
inside .soma-button-primary / -danger | A direct-child badge flips to a translucent-white tint automatically (no extra class). |
.soma-badge-overlay | On the HOST element: pins a child badge to its top inline-end corner (RTL-aware), sized down slightly. |
.soma-badge-dot | 8px presence dot, no text — the meaning lives in the host's aria-label. |
JavaScript
None shipped — badges are CSS-only. The one stateful pattern is
keeping a count's two representations in sync: whenever it
changes, write the badge text and the host's
aria-label from the same function.
function setApprovalCount(n) {
const host = document.getElementById('approvals-button');
const bubble = host.querySelector('.soma-badge');
bubble.textContent = String(n);
bubble.style.display = n === 0 ? 'none' : '';
host.setAttribute('aria-label',
n === 0 ? 'Approvals, none pending' : `Approvals, ${n} pending`);
}
The live example above, exactly as this page wires it:
const host = document.getElementById('badge-live');
const bubble = document.getElementById('badge-live-count');
let approvals = 3;
function render() {
bubble.textContent = String(approvals);
bubble.style.display = approvals === 0 ? 'none' : '';
host.setAttribute('aria-label',
approvals === 0 ? 'Approvals, none pending' : `Approvals, ${approvals} pending`);
}
document.getElementById('badge-inc').addEventListener('click', () => {
approvals += 1;
render();
});
document.getElementById('badge-clear').addEventListener('click', () => {
approvals = 0;
render();
});