← Back to gallery
CSS

Notification Badge Bounce Retrigger

Three notification-badge bounce treatments tuned per product context — an Inbox sidebar row (subtle 1.25x shoulder pop), a Cart Add-to-cart CTA (heavier 1.45x bounce with a rotate kick on the icon), and an alerts Bell with a tall 1.55x overshoot and a parallel bell shake. Each badge keys its DOM node to the current count so React remounts it on every change, replaying the keyframe from frame 0 without animationend bookkeeping.

notification-badgebouncecount-popretriggerreduced-motion

Feedback · transform scale · key-driven retrigger

Notification Badge Bounce Retrigger

Three notification-badge bounce treatments tuned per product context — an Inbox sidebar row (subtle shoulder pop), a Cart header CTA (confident purchase pop with a slight rotate), and an alerts Bell with a tall overshoot. Each badge keys its DOM node to the current count so React remounts it on every change, so the @keyframes runs from the start without needing animationIteration tricks. Stage cards JS-bump the count every 2s to keep the bounce replaying; inspector preview adds an interactive +1/reset and Tab is captured to refocus the demo control.

Sidebar nav · subtle shoulder pop

Inbox

A tight 1.25x scale bounce on the envelope badge signals new unread mail without yanking attention away from the rest of the sidebar. Suited to high-frequency updates like email rows — the pop is quick and returns to rest before the user finishes scanning adjacent items.

  • inbox
  • small scale
  • high frequency

Header CTA · confident purchase pop

Cart

A heavier 1.45x bounce paired with a slight rotate conveys the warmth of a confirmed add-to-cart action. Lives in a header-style chip with the cart label so the bounce reads as intentional product feedback rather than ambient noise.

  • cart
  • add to cart
  • rotate kick

Top nav · tall alerts overshoot

Bell

A 1.55x bounce with a deliberate overshoot is right when the alerts bell gains a new notification and the count needs to read high. The bell itself shakes once on the same keyframe so the gesture is unmistakable.

  • bell
  • alerts
  • overshoot + shake

Notification badge inspector

Inbox

click badge
  • inbox
  • small scale
  • high frequency

A tight 1.25x scale bounce on the envelope badge signals new unread mail without yanking attention away from the rest of the sidebar. Suited to high-frequency updates like email rows — the pop is quick and returns to rest before the user finishes scanning adjacent items.

Helped you ship something? 🐟 Send my cat a churu

/* Inbox sidebar row — tight 1.25x shoulder pop on the envelope badge. */
.badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 6px;
  border-radius: 999px;
  background: #7dd3fc;
  color: #0f172a;
  font-size: 11px;
  font-weight: 700;
  transform-origin: center;
}

/* Re-key the React node on every count change so the keyframe
   replays from the start without animationend bookkeeping. */
.badge { animation: badgePop 0.42s cubic-bezier(0.34, 1.56, 0.64, 1); }

@keyframes badgePop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.25); }
  100% { transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .badge { animation: none; }
}

How to make this

A notification badge bounce retriggers by inserting or re-keying the badge node on count changes, while transform handles the visible pop.

html
1<button class="badge-trigger" type="button" aria-label="Inbox, 4 unread">  <span class="badge-trigger__icon" aria-hidden="true">Mail</span>  Inbox4  <span class="badge-trigger__count" aria-live="polite">    <span class="badge-trigger__badge">4</span>    <span class="badge-trigger__sr"> unread messages</span>  </span></button> <style>.badge-trigger {  position: relative;  display: inline-flex;  align-items: center;  gap: .6rem;  border: 1px solid #334155;  border-radius: 12px;  padding: .65rem .9rem;  color: #e2e8f0;  background: #0f172a;  font: 700 .92rem/1 ui-sans-serif, system-ui;}.badge-trigger__icon { font-size: 1.1rem; }.badge-trigger__badge {25  position: absolute;  inset-block-start: -.55rem;  inset-inline-end: -.55rem;  display: inline-grid;  place-items: center;  min-inline-size: 1.25rem;  block-size: 1.25rem;  padding-inline: .35rem;  border-radius: 999px;  color: #0f172a;  background: #7dd3fc;  font-size: .72rem;37  transform-origin: center;38  animation: notification-badge-recipe-pop .42s cubic-bezier(.34,1.56,.64,1);}.badge-trigger__sr {  position: absolute;  inline-size: 1px;  block-size: 1px;  overflow: hidden;  clip: rect(0 0 0 0);}@keyframes notification-badge-recipe-pop {  0%, 100% { transform: scale(1); }  45% { transform: scale(1.35); }}51@media (prefers-reduced-motion: reduce) {  .badge-trigger__badge { animation: none; }}</style>

Annotated snippet

  1. Line 1The trigger carries a complete label with the current count. The badge is compact visual feedback, not the only accessible notification text.
    PitfallShould notification badges use aria-live?

    Use a polite live region for routine count changes, such as unread messages or cart items. Use assertive only for urgent alerts, because it can interrupt what a screen reader is already announcing.

  2. Line 4The count wrapper can be a polite live region for normal updates. Use assertive only for urgent alerts that should interrupt screen reader output.
    PitfallShould notification badges use aria-live?

    Use a polite live region for routine count changes, such as unread messages or cart items. Use assertive only for urgent alerts, because it can interrupt what a screen reader is already announcing.

  3. Line 25The badge is absolutely positioned relative to the trigger, so the pop does not resize or push the button label around.
    PitfallWhy animate transform instead of badge size?

    Transform creates a pop without changing the badge layout box. Animating width, padding, or font-size can shove nearby labels and make the trigger feel unstable.

  4. Line 37The pop uses transform scale. Avoid animating font-size, width, or padding for badge feedback because those change the measured pill.

    Watch the "msgs" label on the right. Animating padding reflows the badge pill and shoves the neighbour each pop; transform scales the pill in place so the row stays still.

    PitfallWhy animate transform instead of badge size?

    Transform creates a pop without changing the badge layout box. Animating width, padding, or font-size can shove nearby labels and make the trigger feel unstable.

  5. Line 38The animation name stays on the badge. To retrigger it on every count change, reinsert the badge, change its key, or briefly remove and re-add the animation class.

    A text-only count update changes 3 to 4 without restarting the badge; a re-keyed badge plays the pop on that same update.

    PitfallWhy does my badge bounce only the first time?

    CSS animations start when the animated style is applied. If the same DOM node keeps the same class, a count text update will not restart the keyframe. Re-key or remount the badge, or remove and re-add the animation class.

  6. Line 51Reduced motion stops the bounce but leaves the count visible. The notification state should not depend on motion.
    PitfallWhat should reduced motion do for badge updates?

    Disable the bounce and keep the new count visible and announced. Users who reduce motion still need the updated notification number.

Other pitfalls

Can badge bounce animations be too noisy?
Yes. High-frequency counts should use a small scale and short duration. Reserve larger overshoot, rotation, or bell shake for rare events that genuinely need attention.
Advanced

Spring-damped settle — two-tier overshoot instead of a single peak

Same .badge-trigger__badge element, same one-shot trigger, same transform: scale property, same final rest scale (1). BEFORE uses a single overshoot keyframe (1 → 1.35 → 1) with cubic-bezier overshoot — the badge pops, peaks, and lands cleanly on rest with no oscillation. AFTER keeps the same peak (1.35) but lets it swing past rest into a counter-bounce (.92), then back up to a small re-overshoot (1.05), micro counter (.98), and finally settles. The post-peak oscillation is what makes the badge feel like it has mass instead of snapping into place by command.

View explanation and full code21 lines

The base pop is a clean one-shot scale 1 → 1.35 → 1 with a cubic-bezier overshoot — it does the job, but the badge "lands" at exactly the rest scale on the first try, which physics never does. A real spring overshoots, swings past the resting position, oscillates back smaller, and settles. Same one-shot trigger (.badge-trigger__badge animation that fires once per render), same transform: scale property, same destination (scale 1) — what changes is the keyframe between: peak overshoot (1.35) → counter-swing under rest (.92) → small re-overshoot (1.05) → settle (1). The badge looks like it has mass.

Append these rules inside the <style> block from the base snippet above.

css
/* Advanced: spring-damped settle — extends the base recipe.   Same one-shot animation triggered on render/key-change, same   transform: scale property, same final value (1). What changes is the   keyframe choreography between: peak overshoot → counter-swing below   rest → small re-overshoot → settle. cubic-bezier easing replaced by   linear so the keyframe stops carry the shape; the animation duration   bumps from 420ms to 560ms to give the secondary swings room. */.badge-trigger__badge {  animation: notification-badge-recipe-settle 560ms linear both;}@keyframes notification-badge-recipe-settle {  0%   { transform: scale(1); }  28%  { transform: scale(1.35); }  50%  { transform: scale(0.92); }  72%  { transform: scale(1.05); }  88%  { transform: scale(0.98); }  100% { transform: scale(1); }}@media (prefers-reduced-motion: reduce) {  .badge-trigger__badge { animation: none; }}

Notes

Overview

Notification badges bounce when the count changes. The trick is keying the badge DOM node to the current count value so the framework (or, in vanilla JS, an explicit node clone) remounts the badge on every change — a fresh node has no in-flight animation, so the keyframe runs from frame 0 every time. Three variants ship: a subtle inbox shoulder-pop, a confident cart purchase-pop with rotate, and a tall alerts overshoot bell.

When to use it

Reach for the badge bounce on inbox icons, cart counters, alert bells, any persistent badge whose count changes. Skip it for badges that update frequently (every second) — the constant bouncing is distracting. Skip it on badges that already pulse for other reasons (live indicators); the two motions fight each other.

How it works

In React, set <Badge key={count}>{count}</Badge> — when count changes, React unmounts the previous badge node and mounts a fresh one, so the CSS animation starts from frame 0 on a brand-new element. In vanilla JS, either clone the badge node with node.cloneNode(true) and replace it, or use the same offsetWidth-reflow trick as the error-shake. The keyframe itself is a 4-stop bounce ramp (scale up, overshoot, settle) with the cart variant adding a brief rotate(-8deg) on the overshoot beat.

Production gotchas

Without the key trick, the second count change triggers no animation at all and looks broken — this is the most common badge bug. Don’t key on a random ID; key on the count value itself so equal-to-previous counts (e.g., an unread-then-read cycle landing back on the same number) also retrigger. The overshoot scale can push the badge past adjacent layout; reserve a tiny margin around the badge or anchor it with position: absolute on the parent. Avoid retriggering on every keystroke if the count is debounced upstream — the badge becomes a metronome.

Accessibility

Pair the badge with an aria-live="polite" sibling that announces the new count text (“3 unread messages”) so screen readers hear the change. The badge itself can use aria-hidden="true" since the accessible announcement lives elsewhere. Under prefers-reduced-motion: reduce drop the bounce keyframe and rely on the count-text update plus a brief color flash for the visual signal.

References

Implementation depth

A badge bounce is useful only when the count changes. Restart the animation from a state key or class toggle tied to the new value, otherwise repeated updates can be swallowed by an already-running keyframe.

The badge still needs a textual label. Color and bounce alone do not explain what changed, so expose the count in the accessible name and use reduced motion to keep the count update visible without the pop.