16 CSS Bounce Animations09 / 16

CSS + JSMIT licensed

Notification Badge Pop

A bell icon with a red count badge that pops into view when a new notification arrives — the bell shakes briefly and the badge scales up with elastic overshoot. Slack / Discord / Notion header notification pattern. The demo centers a large bell + badge as the hero — the bounce reads as "new event arrived NOW."

Published

Live Demo
Try it

The code

<div class="bn-09">
  <p class="bn-09__label">New notification arriving</p>
  <button class="bn-09__bell" id="bn-09-bell" aria-label="Notifications, 0 unread">
    <svg viewBox="0 0 24 24" width="44" height="44" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <path d="M6 8a6 6 0 0112 0c0 7 3 9 3 9H3s3-2 3-9"/>
      <path d="M10.3 21a1.94 1.94 0 003.4 0"/>
    </svg>
    <span class="bn-09__badge" id="bn-09-badge" aria-hidden="true">0</span>
  </button>
  <p class="bn-09__hint">The bell rings + badge pops each time a new event arrives.</p>
  <div class="bn-09__live" role="status" aria-live="polite" id="bn-09-live"></div>
</div>
.bn-09 {
  --page-bg: #faf7ff;
  --surface: #ffffff;
  --ink: #1c1917;
  --sub: #78716c;
  --slack: #4a154b;
  --red: #ef4444;
  --line: #e7e5e0;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 32px;
  background: radial-gradient(60% 60% at 50% 50%, rgba(74,21,75,.06) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
  text-align: center;
}

.bn-09 *,
.bn-09 *::before,
.bn-09 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bn-09 ::selection {
  background: var(--slack);
  color: #fff;
}

.bn-09__label {
  font-size: .9rem;
  font-weight: 600;
  color: var(--sub);
  letter-spacing: .02em;
}

.bn-09__bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 88px;
  height: 88px;
  border: none;
  border-radius: 14px;
  background: var(--surface);
  color: var(--slack);
  cursor: pointer;
  box-shadow: 0 12px 30px -8px rgba(74,21,75,.18),0 4px 8px -4px rgba(74,21,75,.08);
  transition: background .18s ease;
  will-change: transform;
}

.bn-09__bell:hover {
  background: #f9f4fa;
}

.bn-09__bell:focus-visible {
  outline: 2px solid var(--slack);
  outline-offset: 4px;
}

.bn-09__bell.is-ringing svg {
  animation: bn-09-bell-shake .7s ease-out;
  transform-origin: top center;
}

@keyframes bn-09-bell-shake {
  0% {
    transform: rotate(0);
  }

  15% {
    transform: rotate(-15deg);
  }

  30% {
    transform: rotate(12deg);
  }

  45% {
    transform: rotate(-8deg);
  }

  60% {
    transform: rotate(4deg);
  }

  75% {
    transform: rotate(-2deg);
  }

  100% {
    transform: rotate(0);
  }
}

.bn-09__badge {
  position: absolute;
  top: 8px;
  right: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 24px;
  padding: 0 7px;
  border-radius: 12px;
  background: var(--red);
  color: #fff;
  font-size: .75rem;
  font-weight: 800;
  border: 3px solid var(--surface);
  font-variant-numeric: tabular-nums;
  opacity: 0;
  transform: scale(0);
  will-change: transform;
}

.bn-09__badge.is-visible {
  opacity: 1;
  transform: scale(1);
}

.bn-09__badge.is-popping {
  animation: bn-09-badge-pop .55s cubic-bezier(.34,1.56,.64,1) forwards;
}

@keyframes bn-09-badge-pop {
  0% {
    transform: scale(0);
    opacity: 0;
  }

  30% {
    transform: scale(1.3);
    opacity: 1;
  }

  50% {
    transform: scale(.9);
  }

  70% {
    transform: scale(1.08);
  }

  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.bn-09__hint {
  font-size: .78rem;
  color: var(--sub);
  max-width: 320px;
  line-height: 1.5;
}

.bn-09__live {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

@media (prefers-reduced-motion: reduce) {
  .bn-09__bell.is-ringing svg {
    animation: none!important;
  }

  .bn-09__badge.is-popping {
    animation: none!important;
  }

  .bn-09__badge.is-visible {
    transform: scale(1)!important;
    opacity: 1!important;
  }
}
(function(){
  var root = document.querySelector('.bn-09');
  if (!root) return;
  var bell = root.querySelector('#bn-09-bell');
  var badge = root.querySelector('#bn-09-badge');
  var live = root.querySelector('#bn-09-live');
  if (!bell || !badge) return;
  var count = 0;
  function ring(){
    count++;
    badge.textContent = String(count);
    bell.setAttribute('aria-label', 'Notifications, ' + count + ' unread');
    if (live) live.textContent = count + ' new notification' + (count === 1 ? '' : 's');
    badge.classList.add('is-visible');
    bell.classList.add('is-ringing');
    badge.classList.add('is-popping');
    setTimeout(function(){
      bell.classList.remove('is-ringing');
      badge.classList.remove('is-popping');
    }, 750);
  }
  function reset(){ count = 0; badge.textContent = '0'; badge.classList.remove('is-visible'); bell.setAttribute('aria-label', 'Notifications, 0 unread'); }
  setInterval(function(){ if (count >= 4) { reset(); return; } ring(); }, 2600);
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Bounce Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Notification Badge Pop
Source: https://codefronts.com/motion/css-bounce-animation/notification-badge-pop/

A bell icon with a red count badge that pops into view when a new notification arrives — the bell shakes briefly and the badge scales up with elastic overshoot. Slack / Discord / Notion header notification pattern. The demo centers a large bell + badge as the hero — the bounce reads as "new event arrived NOW."
## HTML
```html
<div class="bn-09">
  <p class="bn-09__label">New notification arriving</p>
  <button class="bn-09__bell" id="bn-09-bell" aria-label="Notifications, 0 unread">
    <svg viewBox="0 0 24 24" width="44" height="44" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
      <path d="M6 8a6 6 0 0112 0c0 7 3 9 3 9H3s3-2 3-9"/>
      <path d="M10.3 21a1.94 1.94 0 003.4 0"/>
    </svg>
    <span class="bn-09__badge" id="bn-09-badge" aria-hidden="true">0</span>
  </button>
  <p class="bn-09__hint">The bell rings + badge pops each time a new event arrives.</p>
  <div class="bn-09__live" role="status" aria-live="polite" id="bn-09-live"></div>
</div>
```
## CSS
```css
.bn-09 {
  --page-bg: #faf7ff;
  --surface: #ffffff;
  --ink: #1c1917;
  --sub: #78716c;
  --slack: #4a154b;
  --red: #ef4444;
  --line: #e7e5e0;
  font-family: -apple-system,BlinkMacSystemFont,'Inter','Segoe UI',sans-serif;
  width: 100%;
  min-height: 100vh;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding: 32px;
  background: radial-gradient(60% 60% at 50% 50%, rgba(74,21,75,.06) 0%, transparent 60%), var(--page-bg);
  color: var(--ink);
  text-align: center;
}

.bn-09 *,
.bn-09 *::before,
.bn-09 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.bn-09 ::selection {
  background: var(--slack);
  color: #fff;
}

.bn-09__label {
  font-size: .9rem;
  font-weight: 600;
  color: var(--sub);
  letter-spacing: .02em;
}

.bn-09__bell {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 88px;
  height: 88px;
  border: none;
  border-radius: 14px;
  background: var(--surface);
  color: var(--slack);
  cursor: pointer;
  box-shadow: 0 12px 30px -8px rgba(74,21,75,.18),0 4px 8px -4px rgba(74,21,75,.08);
  transition: background .18s ease;
  will-change: transform;
}

.bn-09__bell:hover {
  background: #f9f4fa;
}

.bn-09__bell:focus-visible {
  outline: 2px solid var(--slack);
  outline-offset: 4px;
}

.bn-09__bell.is-ringing svg {
  animation: bn-09-bell-shake .7s ease-out;
  transform-origin: top center;
}

@keyframes bn-09-bell-shake {
  0% {
    transform: rotate(0);
  }

  15% {
    transform: rotate(-15deg);
  }

  30% {
    transform: rotate(12deg);
  }

  45% {
    transform: rotate(-8deg);
  }

  60% {
    transform: rotate(4deg);
  }

  75% {
    transform: rotate(-2deg);
  }

  100% {
    transform: rotate(0);
  }
}

.bn-09__badge {
  position: absolute;
  top: 8px;
  right: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  height: 24px;
  padding: 0 7px;
  border-radius: 12px;
  background: var(--red);
  color: #fff;
  font-size: .75rem;
  font-weight: 800;
  border: 3px solid var(--surface);
  font-variant-numeric: tabular-nums;
  opacity: 0;
  transform: scale(0);
  will-change: transform;
}

.bn-09__badge.is-visible {
  opacity: 1;
  transform: scale(1);
}

.bn-09__badge.is-popping {
  animation: bn-09-badge-pop .55s cubic-bezier(.34,1.56,.64,1) forwards;
}

@keyframes bn-09-badge-pop {
  0% {
    transform: scale(0);
    opacity: 0;
  }

  30% {
    transform: scale(1.3);
    opacity: 1;
  }

  50% {
    transform: scale(.9);
  }

  70% {
    transform: scale(1.08);
  }

  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.bn-09__hint {
  font-size: .78rem;
  color: var(--sub);
  max-width: 320px;
  line-height: 1.5;
}

.bn-09__live {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

@media (prefers-reduced-motion: reduce) {
  .bn-09__bell.is-ringing svg {
    animation: none!important;
  }

  .bn-09__badge.is-popping {
    animation: none!important;
  }

  .bn-09__badge.is-visible {
    transform: scale(1)!important;
    opacity: 1!important;
  }
}
```

## JavaScript
```js
(function(){
  var root = document.querySelector('.bn-09');
  if (!root) return;
  var bell = root.querySelector('#bn-09-bell');
  var badge = root.querySelector('#bn-09-badge');
  var live = root.querySelector('#bn-09-live');
  if (!bell || !badge) return;
  var count = 0;
  function ring(){
    count++;
    badge.textContent = String(count);
    bell.setAttribute('aria-label', 'Notifications, ' + count + ' unread');
    if (live) live.textContent = count + ' new notification' + (count === 1 ? '' : 's');
    badge.classList.add('is-visible');
    bell.classList.add('is-ringing');
    badge.classList.add('is-popping');
    setTimeout(function(){
      bell.classList.remove('is-ringing');
      badge.classList.remove('is-popping');
    }, 750);
  }
  function reset(){ count = 0; badge.textContent = '0'; badge.classList.remove('is-visible'); bell.setAttribute('aria-label', 'Notifications, 0 unread'); }
  setInterval(function(){ if (count >= 4) { reset(); return; } ring(); }, 2600);
})();
```

How this works

Two coordinated animations fire when a new notification arrives: (1) the bell shakes via @keyframes bn-09-bell-shake — a 6-stop rotate cascade from 0° → -15° → 12° → -8° → 4° → 0° over 700ms. This is the classic "physical bell being rung" motion. (2) The badge count element pops via @keyframes bn-09-badge-pop — starts at scale(0) (invisible) and animates through 5 stops: 0% scale(0) → 30% scale(1.3) → 50% scale(.9) → 70% scale(1.08) → 100% scale(1). The overshoot-then-settle mimics a real spring landing. Duration 550ms with cubic-bezier(.34, 1.56, .64, 1).

The two animations start simultaneously — the bell shakes AND the badge pops in the same instant. This is different from Demo #08 (cart) where the badge starts 100ms after the cart shake. For notifications, simultaneity reads as "new event arrived NOW." For cart, sequencing reads as "item landed → count updated." Match motion sequencing to the mental model.

The demo centers a large bell (44px SVG) with its badge as the hero — no Slack sidebar, no workspace context, no channels. Just the two elements the bounce happens on.

Make it yours

  • Change bell shake intensity by editing peak rotate(-15deg) — 8deg subtle, 22deg dramatic.
  • Add a color pulse to the badge — box-shadow: 0 0 0 6px rgba(239, 68, 68, .3) → 0 0 0 0 transparent outward-cast ring. Pulls the eye without additional geometry.
  • For platforms with multiple notification types, use different badge colors — Slack uses red for DMs, aubergine dot for channel unread, no badge for muted.
  • For platforms without a bell (Discord, Telegram), apply the same badge pop to any icon.
  • For Tailwind: animate-[bn09_.55s_cubic-bezier(.34,1.56,.64,1)_forwards] on the badge + animate-[bn09b_.7s_ease-out_forwards] on the bell. Register both keyframes. See FAQ #4.

Gotchas — read before shipping

  • The badge must be position: absolute anchored to the bell, and the bell wrapper must be position: relative. Wrong anchor → badge displaced or clipped.
  • The bell shake uses rotate — but if the button also has a hover transform: translateY(-1px), the shake replaces the hover translate. Put hover translate on a wrapper, or include hover state at 100% in the shake keyframe.
  • For accessibility, the button's aria-label MUST update dynamically when count changes. Screen readers announce current label on focus — stale label is worse than none.
  • Do NOT trigger the badge pop on EVERY badge update — if count goes 3→4→5→6 rapidly, badge constantly bounces. Debounce: only pop when transitioning from 0 → N or when the pop hasn't fired in the last 500ms.
  • For platforms where users DISABLE notifications, muted channels still need visible-but-non-animated badges. Visual indicator persists (count still visible), pop animation disabled.
  • Do NOT use role="alert" on badge or button — alert is aria-live=assertive and interrupts screen readers. Use aria-live="polite".
  • For prefers-reduced-motion, keep badge VISIBLE and count incremented, but disable pop scale + bell shake. Information preserved.

Browser support

ChromeSafariFirefoxEdge
45+10+40+45+

transform + @keyframes + class toggle. Universal, GPU-accelerated.

Techniques used in this demo

Search CodeFronts

Loading…