Number Badge
Notification counters and presence dots - positioned on icons, buttons, or avatars with a clean JavaScript API
Overview
Number Badge extends Domma's badge system with positioned notification counters and presence dots. Use CSS-only classes for static indicators, or the JavaScript API to programmatically manage counters on any element - icons, buttons, avatars, and more.
Key Features
- CSS-Only - standalone circular badges and dot indicators, no JS required
- JS API - attach and manage counters on any element with
E.numberBadge() - Web Component - declarative
<domma-number-badge>with slot support - Auto-Hide - badge hides automatically when count reaches zero
- Dot Mode - small presence indicator with no text
- Pulse Animation - draw attention with a pulsing ring
- 8 Colour Variants - matches all Domma semantic colours
At a Glance
<div class="demo-counters-row">
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<button type="button" class="btn btn-outline"><span data-icon="bell"></span></button>
<span class="badge-counter">5</span>
</div>
<span class="demo-label">CSS-only</span>
</div>
<div class="demo-target-label">
<button type="button" id="overview-js-target" class="btn btn-outline"><span data-icon="mail"></span></button>
<span class="demo-label">JS API</span>
</div>
<div class="demo-target-label">
<domma-number-badge count="12" variant="primary">
<button type="button" class="btn btn-outline"><span data-icon="shopping-cart"></span></button>
</domma-number-badge>
<span class="demo-label">Web Component</span>
</div>
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<div class="demo-avatar">JD</div>
<span class="badge-counter badge-dot badge-counter-success"></span>
</div>
<span class="demo-label">Dot mode</span>
</div>
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<button type="button" class="btn btn-outline"><span data-icon="bell"></span></button>
<span class="badge-counter badge-counter-warning badge-pulse">!</span>
</div>
<span class="demo-label">Pulse</span>
</div>
</div>/**
* At a Glance - from public/showcase/elements/number-badge.
*/
export function mount(root) {
// Overview quick demo - JS API target
if ($(root).find('#overview-js-target').length) {
E.numberBadge(root.querySelector('#overview-js-target'), { count: 9, variant: 'danger' });
}
}// CSS-only counter
<div class="badge-counter-wrapper">
<button class="btn"><span data-icon="bell"></span></button>
<span class="badge-counter">5</span>
</div>
// JavaScript API
const nb = E.numberBadge('#bell-btn', { count: 5, variant: 'danger' });
nb.increment(); // → 6
nb.setCount(0); // badge hides automatically
// Web Component
<domma-number-badge count="5" variant="primary">
<button class="btn"><span data-icon="bell"></span></button>
</domma-number-badge>
CSS-Only Usage
No JavaScript required. Apply CSS classes directly for static indicators.
Standalone Number Badges
Add .badge-number to any inline element. Single digits are circular; multi-digit numbers expand to a pill shape automatically via min-width.
<div class="demo-variants-row">
<span class="badge-number">3</span>
<span class="badge-number badge-number-primary">7</span>
<span class="badge-number badge-number-secondary">12</span>
<span class="badge-number badge-number-success">42</span>
<span class="badge-number badge-number-danger">99</span>
<span class="badge-number badge-number-warning">8</span>
<span class="badge-number badge-number-info">1</span>
<span class="badge-number badge-number-dark">100</span>
<span class="badge-number badge-number-light">5</span>
</div><span class="badge-number">3</span>
<span class="badge-number badge-number-primary">7</span>
<span class="badge-number badge-number-success">42</span>
<span class="badge-number badge-number-danger">99</span>
<span class="badge-number badge-number-warning">8</span>
<span class="badge-number badge-number-info">1</span>
Dot Indicators
Add .badge-dot for a small presence indicator with no text content - ideal for online/offline status on avatars.
<div class="demo-dot-row">
<div class="demo-dot-item">
<span class="badge-dot"></span>
<span class="demo-label">Default</span>
</div>
<div class="demo-dot-item">
<span class="badge-dot badge-dot-primary"></span>
<span class="demo-label">Primary</span>
</div>
<div class="demo-dot-item">
<span class="badge-dot badge-dot-success"></span>
<span class="demo-label">Success</span>
</div>
<div class="demo-dot-item">
<span class="badge-dot badge-dot-danger"></span>
<span class="demo-label">Danger</span>
</div>
<div class="demo-dot-item">
<span class="badge-dot badge-dot-warning"></span>
<span class="demo-label">Warning</span>
</div>
<div class="demo-dot-item">
<span class="badge-dot badge-dot-info"></span>
<span class="demo-label">Info</span>
</div>
</div><span class="badge-dot"></span>
<span class="badge-dot badge-dot-success"></span>
<span class="badge-dot badge-dot-danger"></span>
Positioned Counters
Wrap any element in .badge-counter-wrapper and add a sibling .badge-counter to position a counter at the top-right corner with a border ring.
<div class="demo-counters-row">
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<button type="button" class="btn btn-outline"><span data-icon="bell"></span></button>
<span class="badge-counter">5</span>
</div>
<span class="demo-label">Default</span>
</div>
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<button type="button" class="btn btn-primary"><span data-icon="mail"></span> Inbox</button>
<span class="badge-counter badge-counter-primary">3</span>
</div>
<span class="demo-label">On a button</span>
</div>
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<div class="demo-avatar">JD</div>
<span class="badge-counter badge-dot badge-counter-success"></span>
</div>
<span class="demo-label">Presence dot</span>
</div>
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<button type="button" class="btn btn-outline"><span data-icon="shopping-cart"></span></button>
<span class="badge-counter badge-counter-danger">12</span>
</div>
<span class="demo-label">Cart</span>
</div>
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<button type="button" class="btn btn-outline"><span data-icon="bell"></span></button>
<span class="badge-counter badge-counter-warning badge-pulse">!</span>
</div>
<span class="demo-label">Pulsing</span>
</div>
</div><!-- Notification counter -->
<div class="badge-counter-wrapper">
<button class="btn"><span data-icon="bell"></span></button>
<span class="badge-counter">5</span>
</div>
<!-- Presence dot on avatar -->
<div class="badge-counter-wrapper">
<div class="avatar">JD</div>
<span class="badge-counter badge-dot badge-counter-success"></span>
</div>
<!-- Colour variant -->
<div class="badge-counter-wrapper">
<button class="btn">Cart</button>
<span class="badge-counter badge-counter-danger">12</span>
</div>
<!-- Pulsing counter -->
<div class="badge-counter-wrapper">
<button class="btn"><span data-icon="bell"></span></button>
<span class="badge-counter badge-counter-warning badge-pulse">!</span>
</div>
JavaScript API
Use E.numberBadge(selector, options) to attach a managed counter to any element. The badge tracks state, auto-shows/hides, and exposes a chainable API.
Initialisation
const nb = E.numberBadge('#bell-btn', {
count: 5, // Initial count (default: 0)
variant: 'danger', // Colour variant (default: 'danger')
dot: false, // Dot mode - no text
pulse: false, // Pulse animation
borderColor: null // Custom border ring colour
});
nb.increment(); // +1 → 6
nb.decrement(); // -1 → 5
nb.increment(5); // +5 → 10
nb.setCount(0); // Hides the badge automatically
nb.setCount(3); // Shows the badge again
nb.setVariant('info'); // Change colour
nb.setDot(true); // Switch to dot mode
nb.setPulse(true); // Enable pulse
nb.getCount(); // → current count
nb.remove(); // Remove badge and restore element
Interactive Demo
<div class="flex gap-8 flex-wrap items-start">
<div class="demo-target-label" style="padding-top: 0.5rem;">
<button type="button" id="js-demo-target" class="btn btn-outline btn-lg">
<span data-icon="bell"></span>
</button>
<span class="demo-label">Target element</span>
</div>
<div style="flex: 1; min-width: 220px;">
<div class="flex items-center gap-3 mb-4">
<span class="text-gray-600 text-sm">Count:</span>
<span id="js-count-display" class="live-count-display">0</span>
</div>
<div class="flex gap-2 flex-wrap mb-3">
<button type="button" id="js-decrement" class="btn btn-sm btn-outline">− Decrement</button>
<button type="button" id="js-increment" class="btn btn-sm btn-primary">+ Increment</button>
<button type="button" id="js-add-5" class="btn btn-sm btn-outline">+5</button>
<button type="button" id="js-reset" class="btn btn-sm btn-outline-danger">Reset to 0</button>
</div>
<div class="flex gap-2 flex-wrap mb-3">
<button type="button" id="js-dot-toggle" class="btn btn-sm btn-outline">Enable Dot</button>
<button type="button" id="js-pulse-toggle" class="btn btn-sm btn-outline">Enable Pulse</button>
</div>
<div class="flex gap-2 flex-wrap items-center">
<span class="text-gray-600 text-sm">Variant:</span>
<button type="button" class="btn btn-sm btn-outline variant-btn" data-variant="primary">Primary</button>
<button type="button" class="btn btn-sm btn-outline variant-btn" data-variant="success">Success</button>
<button type="button" class="btn btn-sm btn-outline variant-btn" data-variant="danger">Danger</button>
<button type="button" class="btn btn-sm btn-outline variant-btn" data-variant="warning">Warning</button>
<button type="button" class="btn btn-sm btn-outline variant-btn" data-variant="info">Info</button>
</div>
</div>
</div>/**
* Interactive Demo - from public/showcase/elements/number-badge.
*/
export function mount(root) {
// Interactive JS API demo
let jsBadge = null;
let isDot = false;
let isPulse = false;
if ($(root).find('#js-demo-target').length) {
jsBadge = E.numberBadge(root.querySelector('#js-demo-target'), { count: 0, variant: 'danger' });
const updateDisplay = () => {
$(root).find('#js-count-display').text(jsBadge.getCount());
};
$(root).find('#js-increment').on('click', () => { jsBadge.increment(); updateDisplay(); });
$(root).find('#js-decrement').on('click', () => { jsBadge.decrement(); updateDisplay(); });
$(root).find('#js-add-5').on('click', () => { jsBadge.increment(5); updateDisplay(); });
$(root).find('#js-reset').on('click', () => { jsBadge.setCount(0); updateDisplay(); });
$(root).find('#js-dot-toggle').on('click', function() {
isDot = !isDot;
jsBadge.setDot(isDot);
$(this).text(isDot ? 'Disable Dot' : 'Enable Dot');
});
$(root).find('#js-pulse-toggle').on('click', function() {
isPulse = !isPulse;
jsBadge.setPulse(isPulse);
$(this).text(isPulse ? 'Disable Pulse' : 'Enable Pulse');
});
$('.variant-btn').on('click', function() {
jsBadge.setVariant($(this).data('variant'));
});
}
}Multiple Independent Counters
Each E.numberBadge() call creates an independent instance.
<div class="demo-counters-row mb-4">
<div class="demo-target-label">
<button type="button" id="multi-bell" class="btn btn-outline"><span data-icon="bell"></span></button>
<span class="demo-label">Notifications</span>
</div>
<div class="demo-target-label">
<button type="button" id="multi-mail" class="btn btn-outline"><span data-icon="mail"></span></button>
<span class="demo-label">Messages</span>
</div>
<div class="demo-target-label">
<button type="button" id="multi-cart" class="btn btn-outline"><span data-icon="shopping-cart"></span></button>
<span class="demo-label">Cart</span>
</div>
</div>
<div class="flex gap-2 flex-wrap">
<button type="button" id="multi-add-all" class="btn btn-sm btn-primary">Add to all</button>
<button type="button" id="multi-clear-all" class="btn btn-sm btn-outline">Clear all</button>
</div>/**
* Multiple Independent Counters - from public/showcase/elements/number-badge.
*/
export function mount(root) {
// Multiple counters demo
if ($(root).find('#multi-bell').length) {
const bellB = E.numberBadge(root.querySelector('#multi-bell'), { count: 3, variant: 'danger' });
const mailB = E.numberBadge(root.querySelector('#multi-mail'), { count: 12, variant: 'primary' });
const cartB = E.numberBadge(root.querySelector('#multi-cart'), { count: 0, variant: 'success' });
$(root).find('#multi-add-all').on('click', () => {
bellB.increment();
mailB.increment();
cartB.increment();
});
$(root).find('#multi-clear-all').on('click', () => {
bellB.setCount(0);
mailB.setCount(0);
cartB.setCount(0);
});
}
}const bellBadge = E.numberBadge('#bell', { count: 3, variant: 'danger' });
const mailBadge = E.numberBadge('#mail', { count: 12, variant: 'primary' });
const cartBadge = E.numberBadge('#cart', { count: 0, variant: 'success' });
bellBadge.increment();
mailBadge.setCount(0); // Hides automatically
cartBadge.setCount(5);
Web Component
The <domma-number-badge> custom element uses Shadow DOM and a <slot> to wrap any child content with a positioned counter. Declare it in HTML or create it with JS.
Declarative Usage
<div class="demo-counters-row">
<div class="demo-target-label">
<domma-number-badge count="5">
<button type="button" class="btn btn-outline"><span data-icon="bell"></span></button>
</domma-number-badge>
<span class="demo-label">Default (danger)</span>
</div>
<div class="demo-target-label">
<domma-number-badge count="3" variant="primary">
<button type="button" class="btn btn-outline"><span data-icon="mail"></span></button>
</domma-number-badge>
<span class="demo-label">Primary</span>
</div>
<div class="demo-target-label">
<domma-number-badge count="0" variant="success">
<button type="button" class="btn btn-outline"><span data-icon="check"></span></button>
</domma-number-badge>
<span class="demo-label">Count = 0 (hidden)</span>
</div>
<div class="demo-target-label">
<domma-number-badge dot="" variant="success">
<div class="demo-avatar">AB</div>
</domma-number-badge>
<span class="demo-label">Dot mode</span>
</div>
<div class="demo-target-label">
<domma-number-badge count="7" pulse="" variant="warning">
<button type="button" class="btn btn-outline"><span data-icon="bell"></span></button>
</domma-number-badge>
<span class="demo-label">Pulsing</span>
</div>
</div><!-- Default counter -->
<domma-number-badge count="5">
<button class="btn"><span data-icon="bell"></span></button>
</domma-number-badge>
<!-- Primary variant -->
<domma-number-badge count="3" variant="primary">
<button class="btn">Inbox</button>
</domma-number-badge>
<!-- Presence dot on avatar -->
<domma-number-badge dot variant="success">
<div class="avatar">AB</div>
</domma-number-badge>
<!-- Pulsing counter -->
<domma-number-badge count="7" pulse variant="warning">
<button class="btn"><span data-icon="bell"></span></button>
</domma-number-badge>
Programmatic API
<div class="flex gap-6 flex-wrap items-start">
<div class="demo-target-label" style="padding-top: 0.5rem;">
<domma-number-badge id="wc-demo" count="5" variant="primary">
<button type="button" class="btn btn-outline"><span data-icon="star"></span></button>
</domma-number-badge>
<span class="demo-label">Web component target</span>
</div>
<div class="flex gap-2 flex-wrap items-start" style="padding-top: 0.5rem;">
<button type="button" class="btn btn-sm btn-primary" data-wc-action="increment">+ Increment</button>
<button type="button" class="btn btn-sm btn-outline" data-wc-action="decrement">− Decrement</button>
<button type="button" class="btn btn-sm btn-outline" data-wc-action="setVariant" data-arg="success">Success</button>
<button type="button" class="btn btn-sm btn-outline" data-wc-action="setVariant" data-arg="danger">Danger</button>
<button type="button" class="btn btn-sm btn-outline" data-wc-action="setCount" data-arg="0">Clear</button>
</div>
</div>/**
* Programmatic API - from public/showcase/elements/number-badge.
*/
export function mount(root) {
// The web-component demo buttons. One delegated handler rather than five
// inline onclick attributes, which are the thing .on() exists to replace.
$('body').on('click', '[data-wc-action]', function () {
const el = $(root).find('#wc-demo')[0];
if (!el) return;
// attr() rather than data(): the hook IS the attribute, and reading it
// directly avoids depending on kebab-to-camel key conversion.
const action = $(this).attr('data-wc-action');
const arg = $(this).attr('data-arg');
if (arg === undefined || arg === null) el[action]();
else el[action](action === 'setCount' ? Number(arg) : arg);
});
}const badge = document.getElementById('my-badge');
badge.increment();
badge.decrement();
badge.setCount(10);
badge.setVariant('success');
badge.setDot(true);
badge.setPulse(true);
Real-World Patterns
App Navigation Bar
<div class="mock-navbar">
<span class="font-semibold">MyApp</span>
<div class="flex gap-3 items-center">
<div class="badge-counter-wrapper">
<button type="button" class="btn btn-sm btn-outline"><span data-icon="bell"></span></button>
<span class="badge-counter badge-counter-danger">4</span>
</div>
<div class="badge-counter-wrapper">
<button type="button" class="btn btn-sm btn-outline"><span data-icon="mail"></span></button>
<span class="badge-counter badge-counter-primary">12</span>
</div>
<div class="badge-counter-wrapper">
<div class="demo-avatar" style="width: 2rem; height: 2rem; font-size: 0.7rem;">JD</div>
<span class="badge-counter badge-dot badge-counter-success"></span>
</div>
</div>
</div>Inbox with Unread Counts
<div class="inbox-list">
<div class="inbox-row">
<span>Inbox</span>
<span class="badge-number badge-number-primary">24</span>
</div>
<div class="inbox-row">
<span>Sent</span>
</div>
<div class="inbox-row">
<span>Drafts</span>
<span class="badge-number badge-number-warning">3</span>
</div>
<div class="inbox-row">
<span>Spam</span>
<span class="badge-number badge-number-danger">7</span>
</div>
<div class="inbox-row">
<span>Archived</span>
</div>
</div><div class="nav-item">
Inbox <span class="badge-number badge-number-primary">24</span>
</div>
<div class="nav-item">
Drafts <span class="badge-number badge-number-warning">3</span>
</div>
Online User List
<div class="flex gap-5 flex-wrap">
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<div class="demo-avatar">AL</div>
<span class="badge-counter badge-dot badge-counter-success"></span>
</div>
<span class="demo-label">Online</span>
</div>
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<div class="demo-avatar" style="background: var(--dm-secondary)">BM</div>
<span class="badge-counter badge-dot badge-counter-warning"></span>
</div>
<span class="demo-label">Away</span>
</div>
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<div class="demo-avatar" style="background: var(--dm-text-muted)">CN</div>
<span class="badge-counter badge-dot"></span>
</div>
<span class="demo-label">Offline</span>
</div>
<div class="demo-target-label">
<div class="badge-counter-wrapper">
<div class="demo-avatar" style="background: var(--dm-danger)">DR</div>
<span class="badge-counter badge-dot badge-counter-danger badge-pulse"></span>
</div>
<span class="demo-label">Busy</span>
</div>
</div>Best Practices
Accessibility
- aria-live built-in - The JS API adds
aria-live="polite"androle="status"automatically so screen readers announce count changes. - Label the target - Add descriptive
aria-labelto icon buttons:aria-label="Notifications, 5 unread" - Dot mode - Pair dot-only badges with a visible label or tooltip; the dot alone conveys nothing to screen readers.
Design Guidelines
- Trust auto-hide - The badge hides when count reaches 0; resist the urge to show "0"
- Danger for alerts - Red counters signal unread/urgent items; use primary/info for neutral counts
- Pulse sparingly - Reserve pulse for genuinely urgent alerts; overuse erodes its impact
- Dot for presence - Use dot mode for online/away/offline indicators, not for counts
API Reference
Options
| Option | Type | Default | Description |
|---|---|---|---|
count |
number | 0 |
Initial counter value |
variant |
string | 'danger' |
Colour variant: primary, secondary, success, danger, warning, info, light, dark |
dot |
boolean | false |
Show as a dot (no text) |
pulse |
boolean | false |
Enable pulse animation |
borderColor |
string|null | null |
Custom border ring colour (overrides theme default) |
Methods
| Method | Returns | Description |
|---|---|---|
setCount(n) |
this | Set count to exact value; hides at 0 in non-dot mode |
increment(by?) |
this | Increment count by 1 (or by by) |
decrement(by?) |
this | Decrement count by 1 (or by by); minimum 0 |
getCount() |
number | Return current count |
setVariant(v) |
this | Change colour variant |
setDot(bool) |
this | Toggle dot mode on/off |
setPulse(bool) |
this | Toggle pulse animation on/off |
remove() |
void | Remove counter and restore element to original DOM position |
destroy() |
void | Alias for remove() |
CSS Classes
| Class | Description |
|---|---|
.badge-number |
Standalone circular/pill number badge |
.badge-number-{variant} |
Colour variant (primary, success, danger, warning, info, secondary, light, dark) |
.badge-dot |
Small circular dot indicator (8px) |
.badge-dot-{variant} |
Colour variant for dot |
.badge-counter-wrapper |
Relative-positioned container for element + counter |
.badge-counter |
Absolute-positioned counter at top-right corner |
.badge-counter-{variant} |
Colour variant for counter |
.badge-pulse |
Apply pulse animation to .badge-counter |
.badge-counter-hidden |
Hides the counter (applied automatically when count = 0) |
Config Engine
Number Badge is supported in the declarative config engine via $.setup().
$.setup({
'#bell-btn': {
component: 'numberBadge',
options: {
count: 0,
variant: 'danger'
}
}
});
CSS Customisation
Number Badge uses standard Domma semantic colour tokens. Override globally or per-scope.
| Variable | Controls |
|---|---|
--dm-danger |
Default counter background colour |
--dm-primary |
Primary variant colour |
--dm-success |
Success variant colour |
--dm-background |
Border ring colour (matches page background by default) |
--dm-radius-full |
Border radius (9999px = circular/pill) |
/* Match border ring to a dark card background */
.dark-card .badge-counter {
border-color: var(--dm-surface-2);
}
/* Larger counter text */
.badge-counter {
font-size: 0.75rem;
min-width: 1.5rem;
height: 1.5rem;
}
Full CSS Customisation Cheat-Sheet →
Related Elements
<div class="grid grid-cols-1 gap-4 grid-auto-fit-250"><a href="/showcase/elements/badge" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Badge</h3>
<p class="card-text">Inline text labels and status indicators</p>
</div>
</a>
<a href="/showcase/elements/toast" class="card card-hover">
<div class="card-body">
<h3 class="card-title">Toast</h3>
<p class="card-text">Non-blocking notification messages with auto-dismiss</p>
</div>
</a>
<a href="/showcase/elements/notification" class="card card-hover">
<div class="card-body">
<h3 class="card-title">DesktopNotification</h3>
<p class="card-text">Browser native desktop push notifications</p>
</div>
</a>
</div>