<a id="notificationMenuButton" 
   class="notification-link">
  <span class="notification">
    <span class="notification-icon"></span>
  </span>
</a>

<div class="controls">
  <button class="increase" type="button">Increase</button>
  <button class="decrease" type="button">Decrease</button>
  <button class="set" type="button">Set 100</button>
  <button class="hide" type="button">Set 0</button>
  <button class="get" type="button">Get</button>
</div>
$white: #fff;
$border-radius: .25rem;
$header-height: 70px;
$header-text-color: $white;
$open-sans: 'Open Sans', sans-serif;
$notification-bg-color: #fff;
$notification-text-color: #F74D4D;
$notification-box-size: 18;
$notification-font-size: $notification-box-size / 2;

html, body{
  height: 100%;
}

body {
  background: linear-gradient(90deg, #4984fc 20%, #039ae5 98%);
  padding: 20px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  
  .controls {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
  }
  
  button {
    display: block;
    background: $white;
    border: 1px solid transparent;
    padding: 10px 15px;
    border-radius: 8px;
    color: #039ae5;
    text-transform: uppercase;
    font-size: 10px;
    letter-spacing: 1.6px;
    cursor: pointer;
    outline: none !important;
    will-change: color, background, border;
    transition: .2s border ease-in-out, .2s background ease-in-out, .2s color ease-in-out;
      
    &:hover {
      border: 1px solid $white;
      color: $white;
      background: transparent;
    }
  }
}

@mixin clickable-area($w: 50px, $h: 50px) {
  &:after {
    content: '';
    position: absolute;
    width: 50px;
    height: 50px;
    opacity: 0;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
  }
}

.notification {
  width: 38px;
  height: 38px;
  position: relative;
  display: block;

  &-link {
    position: relative;
    width: 38px;
    height: $header-height;
    cursor: pointer;
    display: block;
    margin: 0 0 20px 0;
    user-select: none;
    @include clickable-area();

    &:hover,
    &:focus {
      .notification-icon:after {
        animation: scale-over 2s ease-out infinite;
      }
    }
  }

  &-icon {
    font-size: 28px;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    color: $header-text-color;

    &:before,
    &:after {
      content: "";
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      will-change: transform, opacity;
      background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMSAyMSI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogbm9uZTsKICAgICAgfQoKICAgICAgLmNscy0yIHsKICAgICAgICBmaWxsOiAjZmZmOwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8ZyBpZD0iR3JvdXBfMSIgZGF0YS1uYW1lPSJHcm91cCAxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDgwIC0xNjUpIj4KICAgIDxyZWN0IGlkPSJSZWN0YW5nbGVfMSIgZGF0YS1uYW1lPSJSZWN0YW5nbGUgMSIgY2xhc3M9ImNscy0xIiB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQ4MCAxNjUpIi8+CiAgICA8cGF0aCBpZD0iYmVsbC1vX2NvcHkiIGRhdGEtbmFtZT0iYmVsbC1vIGNvcHkiIGNsYXNzPSJjbHMtMiIgZD0iTTE3LjMsMTUuN2wtMi4xLTMuMXYtNGE2LjQyMSw2LjQyMSwwLDAsMC00LjYtNi4xVjEuN0ExLjY4NSwxLjY4NSwwLDAsMCw4LjksMCwxLjg0NCwxLjg0NCwwLDAsMCw3LjEsMS43di44QTYuNDIxLDYuNDIxLDAsMCwwLDIuNSw4LjZ2My45TC40LDE1LjZhMS42NjksMS42NjksMCwwLDAtLjEsMS44LDEuNjUsMS42NSwwLDAsMCwxLjUuOUg2QTIuOTg4LDIuOTg4LDAsMCwwLDguOCwyMGgwYTMuMDc3LDMuMDc3LDAsMCwwLDIuOC0xLjhoNC4yYTEuODU5LDEuODU5LDAsMCwwLDEuNS0uOUExLjM0OCwxLjM0OCwwLDAsMCwxNy4zLDE1LjdabS0xLDEuMmEuNTUuNTUsMCwwLDEtLjUuM0gxMC43bC0uMS40YTEuOTIyLDEuOTIyLDAsMCwxLTEuOCwxLjNBMS43NSwxLjc1LDAsMCwxLDcsMTcuNmwtLjEtLjRIMS44YS41NS41NSwwLDAsMS0uNS0uMy42MzguNjM4LDAsMCwxLDAtLjZsMi40LTMuNVY4LjZhNS4xMjcsNS4xMjcsMCwwLDEsNC4xLTVsLjUtLjFWMS43YS41LjUsMCwxLDEsMSwwVjMuNWwuNS4xYTUuMTI3LDUuMTI3LDAsMCwxLDQuMSw1djQuM2wyLjMsMy41QzE2LjQsMTYuNSwxNi40LDE2LjcsMTYuMywxNi45WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDgxLjYxNCAxNjUuNSkiLz4KICA8L2c+Cjwvc3ZnPgo=")
    }

    &:after{
      opacity: 0;
    }
  }

  &-badge {
    display: block;
    position: absolute;
    top: 5px;
    right: 1px;
    background: $notification-bg-color;
    border-radius: 100%;
    overflow: hidden;
    width: #{$notification-box-size}px;
    height: #{$notification-box-size}px;
    border: 2px solid $notification-bg-color;
    will-change: transform, background;
    transition: .2s transform ease-in-out, .2s background ease-in-out;
    transform: scale(0);
    box-sizing: border-box;
    box-shadow: 0 0 0 3px #4984fc;

    &--active {
      transform: scale(1);
    }

    &--limit {
      background: $notification-text-color;
    }
  }

  &-counter {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    color: $notification-text-color;
    font-weight: 600;
    font-family: $open-sans, sans-serif;
    font-size: 11px;
    line-height: $notification-box-size;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    text-align: center;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    letter-spacing: -0.5px;
    transform: translate3d(0, 0, 0) scaleY(1);
    opacity: 1;
    will-change: opacity, transform;
    transition: .3s transform ease-in-out, .3s opacity ease-in-out;

    &--old {
      transform: translate3d(0, 100%, 0) scaleY(0);
      opacity: 0;
    }

    &--new {
      transform: translate3d(0, -100%, 0) scaleY(0);
      opacity: 0;
    }
  }
}

@keyframes scale-over {
  0%, 100% {
    transform: scale(1);
    opacity: .7;
  }
  20% {
    transform: scale(1.5);
    opacity: 0;
  }
  20.0001% {
    transform: scale(1);
    opacity: .7;
  }
}
View Compiled

  class Badge {
    constructor(element, options) {
      this.value = 0;
      this.options = options || {
        badgeClass: 'notification-badge',
        badgeCounterClass: 'notification-counter',
        animationSpeed: 150
      }
      if (!element) return;
      this.element = element;
      this.render();
      this.badgeElement = element.querySelector('.' + this.options.badgeClass);
      this.badgeCounterElement = element.querySelector('.' + this.options.badgeCounterClass);
    }
    
    render() {
      let counter = document.createElement('SPAN');
      counter.className = this.options.badgeClass;
      counter.innerHTML = `<span class="${this.options.badgeCounterClass}">0</span>`;
      
      this.element.appendChild(counter);
    }
    
    set(n) {
      n = n || 0;
      let newCounterElement = this.badgeCounterElement.cloneNode();

      // If value is somehow become wrong, wrong type, less than 0, or NaN. 
      // Then hide everything and log an error.
      if (typeof n != 'number' || n < 0 || isNaN(n)) {
        console.error('Wrong type or n(' + n + ') is less then 0!');
        this.badgeElement.classList.remove('notification-badge--active');
        this.badgeCounterElement.innerHTML = '';

        return false;
      }

      if (n === 0) {
        this.badgeElement.classList.remove('notification-badge--active');
        this.badgeCounterElement.innerHTML = '';

        return false;
      }

      if (n > 99) {
        this.badgeElement.classList.add('notification-badge--limit');
      } else {
        this.badgeElement.classList.remove('notification-badge--limit');
      }

      if (!this.badgeElement.classList.contains('notification-badge--active')) {
        this.badgeElement.classList.add('notification-badge--active');
      }      

      let timer;
      let animate = new Promise((resolve, reject) => {
        newCounterElement.innerHTML = n;
        newCounterElement.classList.add('notification-counter--new');
        this.badgeCounterElement.classList.add('notification-counter--old');
        this.badgeCounterElement.after(newCounterElement);

        if (timer) clearTimeout(timer);
        timer = setTimeout(resolve, 0);
      });

      animate.then(() => {
        newCounterElement.classList.remove('notification-counter--new');
        setTimeout(() => {
          this.badgeCounterElement.remove();
          this.badgeCounterElement = newCounterElement
        }, this.options.animationSpeed);
      }, () => false);
    }

    get() {
      let n = parseInt(this.element.querySelector('.' + this.options.badgeCounterClass).innerHTML) || 0;
      return typeof n != 'number' ? this.value : n;
    }

    decrease(n) {
      n = n || 1;
      this.value = this.get() || 0;

      if (this.value - n < 0) {
        return false;
      }

      this.set(this.value - n);
    }

    increase(n) {
      n = n || 1;
      this.value = this.get() || 0;

      if (this.value + n < 0) {
        return false;
      }

      this.set(this.value + n);
    }
  }


  // API usage example:
  let notificationElement = document.querySelector('.notification');
  let customNotification = new Badge(notificationElement);

  document.querySelector('.increase').addEventListener('click', () => {
    customNotification.increase()
  });
  document.querySelector('.decrease').addEventListener('click', () => {
    customNotification.decrease()
  });
  document.querySelector('.set').addEventListener('click', () => {
    customNotification.set(100)
  });
  document.querySelector('.hide').addEventListener('click', () => {
    customNotification.set(0)
  });
  document.querySelector('.get').addEventListener('click', () => {
    window.alert('Notifications count = ' + customNotification.get());
  });
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.