<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());
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.