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

At a Glance
5
CSS-only
JS API
Web Component
JD
Dot mode
!
Pulse
<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.

Standalone Number Badges
3 7 12 42 99 8 1 100 5
<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.

Dot Indicators
Default
Primary
Success
Danger
Warning
Info
<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.

Positioned Counters
5
Default
3
On a button
JD
Presence dot
12
Cart
!
Pulsing
<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

Interactive Demo
Target element
Count: 0
Variant:
<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.

Multiple Independent Counters
Notifications
Messages
Cart
<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

Declarative Usage
Default (danger)
Primary
Count = 0 (hidden)
AB
Dot mode
Pulsing
<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

Programmatic API
Web component target
<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

App Navigation Bar
MyApp
4
12
JD
<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

Inbox with Unread Counts
Inbox 24
Sent
Drafts 3
Spam 7
Archived
<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

Online User List
AL
Online
BM
Away
CN
Offline
DR
Busy
<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" and role="status" automatically so screen readers announce count changes.
  • Label the target - Add descriptive aria-label to 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

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>