<div class="demo">
<a id="notificationMenuButton"
class="notification__link">
<span class="notification">
<span class="notification__icon"></span>
</span>
</a>
<a id="cartMenuButton"
class="cart__link">
<span class="cart">
<span class="cart__icon"></span>
</span>
</a>
</div>
<div class="controls">
<button class="increase" type="button">Increase</button>
<button class="decrease" type="button">Decrease</button>
<button class="hide" type="button">Set 0</button>
<button class="set" type="button">Set 100</button>
<button class="get" type="button">Get</button>
</div>
:root {
--badge-bg-color: #fff;
--badge-text-color: #F74D4D;
--badge-box-size: 20;
--animation-speed: .4s;
--badge-font-size: 13px;
}
@layer support {
html, body{
height: 100%;
}
body {
--body-bg: #4984fc;
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: #fff;
border: 1px solid transparent;
padding: 10px 15px;
border-radius: 4px;
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 #fff;
color: #fff;
background: transparent;
}
}
}
.demo {
display: flex;
flex-direction: row;
gap: 50px;
margin-bottom: 50px;
}
}
.cart,
.notification {
--direction: 1;
width: 38px;
height: 38px;
position: relative;
display: block;
}
.cart {
--badge-bg-color: #F74D4D;
--badge-text-color: #fff;
--badge-box-size: 16;
--animation-speed: .4s;
--badge-font-size: 10px;
}
.notification {
--badge-bg-color: #fff;
--badge-text-color: #F74D4D;
--badge-box-size: 20;
--animation-speed: .4s;
--badge-font-size: 13px;
}
.cart__link,
.notification__link {
position: relative;
width: 38px;
height: 38px;
cursor: pointer;
display: block;
margin: 0;
user-select: none;
&:after {
content: '';
position: absolute;
width: 50px;
height: 50px;
opacity: 0;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
&:hover,
&:focus {
.cart__icon:after,
.notification__icon:after {
animation: scale-over 2s ease-out infinite;
}
}
}
.cart__icon,
.notification__icon {
font-size: 28px;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
&:before,
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
will-change: transform, opacity;
}
&:after{
opacity: 0;
}
}
.cart__icon:before,
.cart__icon:after {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cg fill='none' stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='8'%3E%3Ccircle cx='90' cy='102' r='7'%3E%3C/circle%3E%3Ccircle cx='50' cy='102' r='8'%3E%3C/circle%3E%3Cpath d='M30 34h83.1c2.8 0 4.7 2.8 3.7 5.4l-15.9 41.4C99.7 84 96.7 86 93.4 86H46.6c-3.8 0-7.1-2.7-7.9-6.5L30 34zm-.1 0-1.5-7.6c-.7-3.7-4-6.4-7.8-6.4h-7.2M90.9 46h3M33 46h48.9'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
.notification__icon:before,
.notification__icon:after {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMSAyMSI+CiAgPGRlZnM+CiAgICA8c3R5bGU+CiAgICAgIC5jbHMtMSB7CiAgICAgICAgZmlsbDogbm9uZTsKICAgICAgfQoKICAgICAgLmNscy0yIHsKICAgICAgICBmaWxsOiAjZmZmOwogICAgICB9CiAgICA8L3N0eWxlPgogIDwvZGVmcz4KICA8ZyBpZD0iR3JvdXBfMSIgZGF0YS1uYW1lPSJHcm91cCAxIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtNDgwIC0xNjUpIj4KICAgIDxyZWN0IGlkPSJSZWN0YW5nbGVfMSIgZGF0YS1uYW1lPSJSZWN0YW5nbGUgMSIgY2xhc3M9ImNscy0xIiB3aWR0aD0iMjEiIGhlaWdodD0iMjEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQ4MCAxNjUpIi8+CiAgICA8cGF0aCBpZD0iYmVsbC1vX2NvcHkiIGRhdGEtbmFtZT0iYmVsbC1vIGNvcHkiIGNsYXNzPSJjbHMtMiIgZD0iTTE3LjMsMTUuN2wtMi4xLTMuMXYtNGE2LjQyMSw2LjQyMSwwLDAsMC00LjYtNi4xVjEuN0ExLjY4NSwxLjY4NSwwLDAsMCw4LjksMCwxLjg0NCwxLjg0NCwwLDAsMCw3LjEsMS43di44QTYuNDIxLDYuNDIxLDAsMCwwLDIuNSw4LjZ2My45TC40LDE1LjZhMS42NjksMS42NjksMCwwLDAtLjEsMS44LDEuNjUsMS42NSwwLDAsMCwxLjUuOUg2QTIuOTg4LDIuOTg4LDAsMCwwLDguOCwyMGgwYTMuMDc3LDMuMDc3LDAsMCwwLDIuOC0xLjhoNC4yYTEuODU5LDEuODU5LDAsMCwwLDEuNS0uOUExLjM0OCwxLjM0OCwwLDAsMCwxNy4zLDE1LjdabS0xLDEuMmEuNTUuNTUsMCwwLDEtLjUuM0gxMC43bC0uMS40YTEuOTIyLDEuOTIyLDAsMCwxLTEuOCwxLjNBMS43NSwxLjc1LDAsMCwxLDcsMTcuNmwtLjEtLjRIMS44YS41NS41NSwwLDAsMS0uNS0uMy42MzguNjM4LDAsMCwxLDAtLjZsMi40LTMuNVY4LjZhNS4xMjcsNS4xMjcsMCwwLDEsNC4xLTVsLjUtLjFWMS43YS41LjUsMCwxLDEsMSwwVjMuNWwuNS4xYTUuMTI3LDUuMTI3LDAsMCwxLDQuMSw1djQuM2wyLjMsMy41QzE2LjQsMTYuNSwxNi40LDE2LjcsMTYuMywxNi45WiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDgxLjYxNCAxNjUuNSkiLz4KICA8L2c+Cjwvc3ZnPgo=");
}
.badge {
display: block;
position: absolute;
top: 5px;
right: 1px;
background: var(--badge-bg-color);
border-radius: 100%;
overflow: hidden;
width: calc(var(--badge-box-size) * 1px);
height: calc(var(--badge-box-size) * 1px);
border: 2px solid var(--badge-bg-color);
will-change: transform, background;
transition: var(--animation-speed) transform ease-in-out, var(--animation-speed) background ease-in-out, var(--animation-speed) box-shadow ease-in-out;
transform: scale(0);
box-sizing: border-box;
box-shadow: 0 0 0 3px var(--body-bg, #fff);
&.badge--active {
transform: scale(1);
}
&.badge--animate {
transform: scale(1.1);
box-shadow: 0 0 0 2px var(--body-bg, #fff);
.badge__inner {
transition: var(--animation-speed) transform ease-in;
transform: translate(0, calc(var(--direction) * 25% - 25%));
}
}
&.badge--limit {
background: var(--badge-text-color);
}
}
.badge__inner {
width: 100%;
height: 200%;
position: absolute;
top: 0;
left: 0;
will-change: transform;
transform: translate(0, calc(var(--direction) * -25% - 25%));
}
.badge__counter {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
color: var(--badge-text-color);
font-weight: 600;
font-family: 'Open Sans', sans-serif;
font-size: max(11px, var(--badge-font-size));
line-height: var(--badge-box-size);
width: 100%;
height: 50%;
top: 0;
left: 0;
text-align: center;
font-smoothing: antialiased;
osx-font-smoothing: grayscale;
letter-spacing: -0.6px;
opacity: 1;
transform: translate(0, 0);
will-change: transform;
transition: none;
&.badge__counter--old {
transform: translate(0, calc(var(--direction) * 50% + 50%));
}
&.badge__counter--new {
transform: translate(0, calc(var(--direction) * -50% + 50%));
}
}
@keyframes scale-over {
0%, 100% {
transform: scale(1);
opacity: .7;
}
20% {
transform: scale(1.5);
opacity: 0;
}
20.0001% {
transform: scale(1);
opacity: .7;
}
}
class Badge {
#class = {
badge: {
node: 'badge',
active: 'badge--active',
limit: 'badge--limit',
animate: 'badge--animate',
inner: 'badge__inner'
},
counter: {
node: 'badge__counter',
old: 'badge__counter--old',
new: 'badge__counter--new'
}
}
constructor(element, options) {
this.options = options || {
start: 0,
animationSpeed: 200
}
if (!element) return;
this.element = element;
this.badgeNode = null;
this.valueNode = null;
this.newValueNode = null;
this.value = parseInt(this.options.start) || 0;
this.render();
this.element.style.setProperty('--direction', 1);
this.element.style.setProperty('--animation-speed', (this.options.animationSpeed || 200) + 'ms');
if (this.value > 0) {
let temp = this.value;
this.show();
}
}
render() {
// create and append
this.badgeNode = document.createElement('DIV');
this.badgeNode.className = this.#class.badge.node;
this.badgeInnerNode = document.createElement('DIV');
this.badgeInnerNode.className = this.#class.badge.inner;
this.valueNode = document.createElement('SPAN');
this.valueNode.classList.add(this.#class.counter.node);
this.valueNode.innerHTML = parseInt(this.options.start) || 0;
this.badgeNode.appendChild(this.badgeInnerNode);
this.badgeInnerNode.appendChild(this.valueNode);
this.element.appendChild(this.badgeNode);
this.newValueNode = this.valueNode.cloneNode();
this.valueNode.classList.add(this.#class.counter.old);
this.newValueNode.classList.add(this.#class.counter.new);
this.valueNode.after(this.newValueNode);
// set events
this.badgeInnerNode.addEventListener('transitionend', (e) => {
this.badgeNode.classList.remove(this.#class.badge.animate);
this.valueNode.innerHTML = this.value;
});
}
set(n) {
n = n || 0;
if (n === this.get()) return;
// 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.hide();
return false;
}
// set new value and calc direction.
this.direction = this.value === n ? 0 : this.value < n ? 1 : -1;
this.value = n;
// hide, if counter goes 0 or lower
if (n === 0) {
this.hide();
this.valueNode.innerHTML = '';
this.newValueNode.innerHTML = '';
return false;
}
// show counter if it's not visible yet.
this.show();
// if counter goes over 99 show only spot insted of numbers
this.tintToggle(n > 99);
// set direction and run animation
this.animate();
}
animate() {
this.newValueNode.innerHTML = this.value;
this.element.style.setProperty('--direction', this.direction);
setTimeout(()=>{
this.badgeNode.classList.add(this.#class.badge.animate);
}, 1);
}
tintToggle(flag) {
this.isTinted = flag;
this.badgeNode.classList[flag ? 'add' : 'remove'](this.#class.badge.limit);
}
show() {
if (this.isVisible) return;
this.isVisible = true;
this.badgeNode.classList.add(this.#class.badge.active);
}
hide() {
if (!this.isVisible) return;
this.isVisible = false;
this.badgeNode.classList.remove(this.#class.badge.active);
}
get() {
return this.value;
}
decrease(n) {
n = n || 1;
this.direction = -1;
this.value = this.get() || 0;
if (this.value - n < 0) {
return false;
}
this.set(this.value - n);
}
increase(n) {
n = n || 1;
this.direction = 1;
this.value = this.get() || 0;
if (this.value + n < 0) {
return false;
}
this.set(this.value + n);
}
}
// API usage example:
let customNotification = new Badge(document.querySelector('.notification'), {
start: 21, // default 0
animationSpeed: 350, // default 200
});
let customCart = new Badge(document.querySelector('.cart')); // default option
document.querySelector('.increase').addEventListener('click', () => {
customNotification.increase();
customCart.increase();
});
document.querySelector('.decrease').addEventListener('click', () => {
customNotification.decrease();
customCart.decrease();
});
document.querySelector('.hide').addEventListener('click', () => {
customNotification.set(0);
customCart.set(0);
});
document.querySelector('.set').addEventListener('click', () => {
customNotification.set(100);
customCart.set(100);
});
document.querySelector('.get').addEventListener('click', () => {
window.alert('Notifications count = ' + customNotification.get() + '; Cart count = ' + customCart.get());
});
This Pen doesn't use any external CSS resources.