Follower Alert
A stream alert for a new follower. The card springs in, the heart badge bursts into confetti with a shock ring, the name bounces in letter by letter, a sheen sweeps across, the heart beats, then it pops away. Loop it with a list of names and each pass shows the next.
New follower
How to use it
- Paste the snippet just before
</head>. - Add
data-follower-alertto an element whose text is the small label, with the follower indata-name. Size it withfont-size. - Call
animacious.followerAlert(el, 'their_name')whenever someone follows.
Options
Add any of these attributes to the same element.
data-name | The follower’s name. Give several, comma-separated, and each loop shows the next. |
data-color | Badge and accent colour. Default #a855f7. |
data-hold | How long it stays on screen. Default 2600 ms. |
data-loop | Come back with the next name, over and over. |
data-gap | Time off screen between loops. Default 900 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Follower Alert: a stream alert for a new follower. The card pops in with
// a springy bounce, the heart badge bursts into confetti and a shock ring,
// the name bounces in letter by letter, a sheen sweeps across, then it
// pops away again.
// Usage: <div data-follower-alert data-name="pixel_panda">New follower</div>
// The element's text is the small label above the name. Size it with
// font-size.
// Optional: data-name (the follower's name; give several, comma-separated,
// and each loop shows the next), data-color (badge and accent colour,
// default #a855f7), data-hold (ms on screen, default 2600), data-loop
// (come back with the next name, over and over), data-gap (ms off screen
// between loops, default 900).
// Script use: animacious.followerAlert(el, 'new_name') shows an alert now.
var DEFAULTS = { color: '#a855f7', hold: 2600, gap: 900, label: 'New follower', name: 'new_follower' };
var BURST = ['#fbbf24', '#f472b6', '#38bdf8', '#4ade80', '#ffffff'];
var css =
'.fla{display:inline-block;position:relative;line-height:1.15;text-align:left;--fla-c:' + DEFAULTS.color + '}' +
'.fla-card{position:relative;display:flex;align-items:center;gap:0.85em;padding:0.7em 1.4em 0.7em 0.7em;border-radius:1.1em;color:#fff;' +
'background:linear-gradient(135deg,rgba(30,20,52,.96),rgba(46,22,82,.96));border:1px solid rgba(255,255,255,.14);' +
'box-shadow:0 0.8em 2em rgba(0,0,0,.4),0 0 0 0.12em color-mix(in srgb,var(--fla-c) 45%,transparent)}' +
'.fla-clip{position:absolute;inset:0;border-radius:inherit;overflow:hidden;pointer-events:none}' +
'.fla-sheen{position:absolute;top:-20%;bottom:-20%;left:0;width:35%;background:linear-gradient(100deg,transparent,rgba(255,255,255,.28),transparent);transform:translateX(-150%) skewX(-18deg)}' +
'.fla-badge{position:relative;flex:none;width:2.7em;height:2.7em}' +
'.fla-icon{position:absolute;inset:0;display:grid;place-items:center;border-radius:50%;background:radial-gradient(circle at 35% 30%,color-mix(in srgb,var(--fla-c) 55%,#fff),var(--fla-c) 70%);box-shadow:0 0 1em color-mix(in srgb,var(--fla-c) 70%,transparent)}' +
'.fla-icon svg{width:1.35em;height:1.35em;fill:#fff}' +
'.fla-ring{position:absolute;inset:0;border-radius:50%;border:0.14em solid var(--fla-c);opacity:0}' +
'.fla-p{position:absolute;left:50%;top:50%;width:0.42em;height:0.42em;margin:-0.21em;border-radius:50%;opacity:0}' +
'.fla-p.fla-sq{border-radius:0.08em}' +
'.fla-text{display:flex;flex-direction:column;gap:0.12em;min-width:0}' +
'.fla-label{font-size:0.7em;font-weight:700;letter-spacing:0.16em;text-transform:uppercase;color:color-mix(in srgb,var(--fla-c) 40%,#fff)}' +
'.fla-name{font-size:1.55em;font-weight:800;letter-spacing:-0.01em;white-space:nowrap}' +
'.fla-ch{display:inline-block;white-space:pre}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var HEART = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 21s-7.5-4.6-9.6-9.2C.9 8.4 3 4.5 6.8 4.5c2.1 0 3.6 1.1 5.2 3 1.6-1.9 3.1-3 5.2-3 3.8 0 5.9 3.9 4.4 7.3C19.5 16.4 12 21 12 21z"/></svg>';
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function opt(el, k) { var v = el.dataset[k]; return v !== undefined && v !== '' ? +v : DEFAULTS[k]; }
function setup(el) {
var d = el.dataset;
var label = el.textContent.trim() || DEFAULTS.label;
var names = (d.name || DEFAULTS.name).split(',').map(function (s) { return s.trim(); }).filter(Boolean);
el.classList.add('fla');
el.style.setProperty('--fla-c', d.color || DEFAULTS.color);
el.setAttribute('role', 'status');
el.innerHTML =
'<div class="fla-card" aria-hidden="true">' +
'<span class="fla-clip"><span class="fla-sheen"></span></span>' +
'<span class="fla-badge"><span class="fla-ring"></span><span class="fla-icon">' + HEART + '</span></span>' +
'<span class="fla-text"><span class="fla-label"></span><span class="fla-name"></span></span>' +
'</div>';
var s = {
names: names, index: 0, label: label,
card: el.querySelector('.fla-card'),
sheen: el.querySelector('.fla-sheen'),
badge: el.querySelector('.fla-badge'),
icon: el.querySelector('.fla-icon'),
ring: el.querySelector('.fla-ring'),
labelEl: el.querySelector('.fla-label'),
nameEl: el.querySelector('.fla-name'),
parts: []
};
s.labelEl.textContent = label;
// Confetti pieces live inside the badge so they burst from its centre.
for (var i = 0; i < 16; i++) {
var p = document.createElement('span');
p.className = 'fla-p' + (i % 3 === 0 ? ' fla-sq' : '');
p.style.background = i % 4 === 0 ? 'var(--fla-c)' : BURST[i % BURST.length];
s.badge.appendChild(p);
s.parts.push(p);
}
el._fla = s;
setName(el, names[0]);
if (!reduced()) anime.utils.set(s.card, { opacity: 0, scale: 0.4 });
}
function setName(el, name) {
var s = el._fla;
s.nameEl.textContent = '';
s.chars = name.split('').map(function (c) {
var sp = document.createElement('span');
sp.className = 'fla-ch';
sp.textContent = c;
s.nameEl.appendChild(sp);
return sp;
});
el.setAttribute('aria-label', s.label + ': ' + name);
}
function show(el, name) {
var s = el._fla;
if (s.tl) s.tl.pause();
clearTimeout(s.wait);
setName(el, name);
var loop = el.dataset.loop !== undefined && el.dataset.loop !== 'false';
var hold = opt(el, 'hold');
anime.utils.set(s.card, { opacity: 0, scale: 0.4, y: '0em', rotate: -4 });
anime.utils.set(s.icon, { scale: 0, rotate: -120 });
anime.utils.set(s.ring, { scale: 1, opacity: 0 });
anime.utils.set(s.labelEl, { opacity: 0, x: '-0.6em' });
anime.utils.set(s.chars, { opacity: 0, y: '0.7em', scale: 0.6 });
anime.utils.set(s.sheen, { x: '-150%' });
anime.utils.set(s.parts, { x: 0, y: 0, scale: 1, opacity: 0, rotate: 0 });
var tl = anime.createTimeline();
tl.add(s.card, { opacity: [0, 1], scale: [0.4, 1], rotate: [-4, 0], duration: 800, ease: 'outElastic(1, .6)' })
.add(s.icon, { scale: [0, 1], rotate: [-120, 0], duration: 700, ease: 'outBack(2.2)' }, 120)
.add(s.ring, { scale: [1, 2.8], opacity: [0.9, 0], duration: 700, ease: 'out(3)' }, 300);
s.parts.forEach(function (p, i) {
var a = (i / s.parts.length) * Math.PI * 2 + anime.utils.random(-0.25, 0.25, 2);
var r = anime.utils.random(2.2, 4.4, 2);
tl.add(p, {
x: Math.cos(a) * r + 'em',
y: Math.sin(a) * r + 'em',
rotate: anime.utils.random(-260, 260),
scale: [1.2, 0],
opacity: [{ to: 1, duration: 60 }, { to: 1, duration: 420 }, { to: 0, duration: 260 }],
duration: 740,
ease: 'out(3)'
}, 300);
});
tl.add(s.labelEl, { opacity: [0, 1], x: ['-0.6em', '0em'], duration: 420, ease: 'out(3)' }, 320)
.add(s.chars, { opacity: [0, 1], y: ['0.7em', '0em'], scale: [0.6, 1], duration: 620, ease: 'outBack(2.6)', delay: anime.stagger(32) }, 420)
.add(s.sheen, { x: ['-150%', '420%'], duration: 900, ease: 'inOut(2)' }, 700)
// a heartbeat while it holds
.add(s.icon, { scale: [1, 1.18, 1, 1.12, 1], duration: 900, ease: 'inOut(2)' }, 1300)
.add(s.card, { opacity: 0, scale: 0.85, y: '-0.4em', duration: 360, ease: 'in(3)' }, 1100 + hold);
tl.then(function () {
if (!loop) return;
s.wait = setTimeout(function () {
s.index = (s.index + 1) % s.names.length;
show(el, s.names[s.index]);
}, opt(el, 'gap'));
});
s.tl = tl;
}
window.animacious = window.animacious || {};
window.animacious.followerAlert = function (el, name) {
if (!el || !el._fla) return;
if (reduced()) { setName(el, name || el._fla.names[0]); return; }
show(el, name || el._fla.names[el._fla.index]);
};
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
var s = e.target._fla;
setTimeout(function () { show(e.target, s.names[0]); }, 250);
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-follower-alert]').forEach(function (el) {
if (el._fla) return;
setup(el);
if (!reduced()) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-follower-alert data-name="pixel_panda">New follower</div>
<script>
// when your streaming tool reports a follow:
animacious.followerAlert(document.querySelector('[data-follower-alert]'), 'nova_streams');
</script>