Navigation✨ Premium

Dynamic Badge Positioning

Counter and status badges anchored to circular avatar corners — click increments the count and replays the spring animation via forced reflow.

CSSBadgeResponsive

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Navigation has 36 effects, including 4 free. Explore the category →

3 usage examples

Messaging sidebar — Contacts and unread counts — Dynamic Badge Positioning example 1

① Messaging sidebar — Contacts and unread counts

WhenThe sidebar of a chat app lists contacts with their unread message count and presence status.
WhyBadges reposition automatically if avatar size changes (compact vs comfortable view) — zero JS adjustment. The spring bounce gives instant feedback for each incoming message.
SettingsAvatars 48 px (compact) or 56 px (comfortable). Counters initialized from API. badgePop duration reduced to 0.3s for a dense UI. Status: .online #10b981, .busy #f59e0b, .offline #6b7280.
Team widget — Availability grid — Dynamic Badge Positioning example 2

② Team widget — Availability grid

WhenA presence panel in a project management tool shows team members who are available, busy, or away in real time.
WhyStatus badges anchored to the bottom-right corner of avatars signal availability at a glance without enlarging member cards. The badge counter can reflect assigned tasks.
SettingsAvatars 56 px, 3-column grid. badgePop 0.25s for frequent updates (polling or WebSocket). Status colors adaptable to team convention.
Comment thread — Authors and reply counters — Dynamic Badge Positioning example 3

③ Comment thread — Authors and reply counters

WhenA blog or forum platform shows comment authors with their recent contribution count.
WhyClicking an avatar replays the spring and draws attention to the contribution — instant feedback without a page reload. The incremental counter simulates new replies arriving.
SettingsBadge color #3b82f6 (blue) to distinguish replies from urgent notifications. Avatars 48 px, .badge-label hidden in dense lists.

How it works

Each .badge-avatar is position: relative, creating a positioning context for its child badges. The .badge-count is placed at top: -4px; right: -4px and the .badge-status at bottom: 0; right: 0 — pixel coordinates relative to the avatar's corners. If the avatar size changes, badges follow the corners automatically with no JS recalculation.

The counter's entry animation, @keyframes badgePop, goes from scale(0) to scale(1) with the curve cubic-bezier(.34, 1.56, .64, 1). The second control value (1.56) intentionally exceeds 1.0 — the curve overshoots past the target before settling back, producing a spring bounce with no animation library.

Each click increments the count then replays the animation in 3 steps: cancellation (badge.style.animation = 'none'), reading badge.offsetHeight to trigger a synchronous reflow that purges the computed style value, then reassigning the animation string. Without this reflow, the browser merges the two style changes into one pass and skips replaying an already-finished animation.

Hover scales the avatar (scale(1.12)) and badge (scale(1.2)) via CSS transitions, both using the same cubic-bezier(.34, 1.56, .64, 1) curve. The badge scales via the selector .badge-avatar:hover .badge-count — secondary visual feedback without a single extra line of JS.

Accessibility

  • prefers-reduced-motion absent: the code does not detect the system motion preference. The badgePop animation and hover transitions play unconditionally. To meet WCAG 2.3.3, add @media (prefers-reduced-motion: reduce) { .badge-count { animation: none; } .badge-avatar { transition: none; } }.
  • The .badge-avatar elements are clickable <div>s with no role="button" or tabindex="0" — they are neither keyboard-focusable nor announced by screen readers. For production use, convert to <button> or add both attributes.
  • Counter contrast: white text (#fff) on red background (#ef4444) → ratio ≈ 4.5:1, WCAG AA met for bold normal-size text.
  • The .badge-label elements (En ligne, Occupé, Absent) provide visible text context but are not linked to the avatar via aria-describedby — screen readers won't automatically associate them with the avatar and its counter.

Browser compatibility

Relies on position: absolute, CSS animation, CSS transition, and forced reflow via offsetHeight — universally supported since 2014.

Chrome 43+✓ Full
Firefox 44+✓ Full
Safari 9+✓ Full
Edge 13+✓ Full
Mobile iOS✓ Touch OK
Android Chrome✓ Full

If CSS animations are disabled (OS setting or very old browser), badges appear in their final position without the spring — layout stays perfectly readable and functional.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="badge-scene">
  <div class="badge-avatar badge-avatar-1">
    <!-- Emoji, image or initials -->
    <span class="badge-count">3</span>
    <span class="badge-status online"></span>
    <span class="badge-label">Online</span>
  </div>
  <!-- Repeat .badge-avatar for each user -->
</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
.badge-count { background } #ef4444 Counter background color. Swap for your brand: #3b82f6 (blue), #10b981 (green), #8b5cf6 (purple).
.badge-status.online/busy/offline { background } #10b981 / #f59e0b / #6b7280 Colors for the three presence states. Adapt to your interface convention.
.badge-avatar { width / height } 60px Circular avatar size. Badges follow the corners at any size — no JS recalculation.
.badge-count { animation: .4s … } 0.4s Spring badgePop duration. 0.25s = dense, reactive UI; 0.6s = dramatic accent for important notifications.
cubic-bezier(.34, 1.56, .64, 1) 1.56 = overshoot Second value > 1 = spring bounce amplitude. 2.0 = very elastic; 1.1 = subtle. Modify in both animation and transition.
.badge-count { top / right } -4px / -4px Pixel offset from the avatar's top-right corner. Adjust if the badge is clipped by an overflow parent.
.badge-count { border } 2px solid #0a0a0f Separator border between badge and avatar. Color must match the scene background for the halo effect.
.badge-label { color } #a5b4fc Status text color below the avatar. Adapt to your theme (white, neutral gray, accent color).

FAQ

Yes. .badge-count and .badge-status use position: absolute with pixel coordinates relative to the corners of .badge-avatar (its position: relative container). Change width and height of .badge-avatar: badges follow automatically, no JS recalculation.
The JS cancels the animation (badge.style.animation = 'none'), reads badge.offsetHeight to trigger a synchronous reflow that purges the computed style value, then reassigns the animation string. Without this reflow, the browser merges the style changes into one pass and skips replaying an already-finished animation — the reliable approach without setTimeout or class toggling.
The script runs once on load using querySelectorAll('.badge-avatar'). For avatars inserted afterwards, re-attach the listener manually on each new element. For frequent insertions, use event delegation on the parent: container.addEventListener('click', e => { const av = e.target.closest('.badge-avatar'); if (!av) return; /* badge logic */ }).