Dynamic Badge Positioning
Counter and status badges anchored to circular avatar corners — click increments the count and replays the spring animation via forced reflow.
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



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
badgePopanimation 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-avatarelements are clickable<div>s with norole="button"ortabindex="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-labelelements (En ligne, Occupé, Absent) provide visible text context but are not linked to the avatar viaaria-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.
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):
<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>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| .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
.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.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.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 */ }).