Subscribe Reminder
A friendly nudge for your viewers. A channel card pops up, a pointer glides in and clicks “Subscribe”, which turns grey with a tick, then it clicks the bell, which fills in and rings. The pointer drifts off and the card tucks away.
Subscribe
How to use it
- Paste the snippet just before
</head>. - Add
data-subscribe-reminderto an element whose text is the button label, and give it your channel name indata-channel. Size it withfont-size.
Options
Add any of these attributes to the same element.
data-channel | Channel name, shown with an initial badge. Leave it out for just the button and bell. |
data-count | A small line under the name, such as “12.4K subscribers”. |
data-done | Button text after the click. Default “Subscribed”. |
data-color | Button colour. Default #ef4444. |
data-loop | Play again after a pause, over and over. |
data-hold | How long the card stays after the bell rings. Default 1600 ms. |
data-gap | Time off screen between loops. Default 900 ms. |
Call animacious.subscribeReminder(el) to play it again, for example every ten minutes.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Subscribe Reminder: a stream overlay that nudges viewers to subscribe. A
// channel card pops up, a pointer glides in and clicks the subscribe
// button (which turns into "Subscribed" with a tick), then clicks the bell,
// which rings, before the pointer drifts off and the card tucks away.
// Usage: <div data-subscribe-reminder data-channel="Pixel Kitchen">Subscribe</div>
// The element's text is the button label. Size it with font-size.
// Optional: data-channel (channel name shown on the card; leave it out for
// just the button and bell), data-count (a line under the name, e.g.
// "12.4K subscribers"), data-done (button text after the click, default
// "Subscribed"), data-color (button colour, default #ef4444), data-loop
// (play again after a pause, over and over), data-hold (ms the card stays
// after the bell rings, default 1600), data-gap (ms off screen between
// loops, default 900).
// Script use: animacious.subscribeReminder(el) plays it once more.
var DEFAULTS = { color: '#ef4444', hold: 1600, gap: 900, label: 'Subscribe', done: 'Subscribed' };
var css =
'.sbr{position:relative;display:inline-block;line-height:1.2;text-align:left;--sbr-c:' + DEFAULTS.color + '}' +
'.sbr-card{position:relative;display:flex;align-items:center;gap:0.75em;padding:0.6em 0.7em;border-radius:0.9em;color:#fff;' +
'background:rgba(17,17,27,.92);border:1px solid rgba(255,255,255,.12);box-shadow:0 0.8em 2em rgba(0,0,0,.4)}' +
'.sbr-av{flex:none;display:grid;place-items:center;width:2.4em;height:2.4em;border-radius:50%;font-weight:800;font-size:1em;' +
'background:linear-gradient(135deg,#f472b6,#a855f7 55%,#38bdf8);color:#fff}' +
'.sbr-who{display:flex;flex-direction:column;min-width:0;margin-right:0.3em}' +
'.sbr-name{font-weight:800;white-space:nowrap}' +
'.sbr-count{font-size:0.72em;opacity:.65;white-space:nowrap}' +
'.sbr-btn{position:relative;overflow:hidden;flex:none;display:inline-flex;align-items:center;gap:0.4em;padding:0.55em 1.05em;border-radius:999px;' +
'background:var(--sbr-c);color:#fff;font-weight:800;font-size:0.9em;letter-spacing:0.01em;white-space:nowrap;transition:background-color .25s,color .25s}' +
'.sbr-btn.sbr-on{background:rgba(255,255,255,.14);color:rgba(255,255,255,.9)}' +
'.sbr-tick{display:none;width:1em;height:1em}' +
'.sbr-on .sbr-tick{display:block}' +
'.sbr-tick path{fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}' +
'.sbr-ripple{position:absolute;left:50%;top:50%;width:1em;height:1em;margin:-0.5em;border-radius:50%;background:rgba(255,255,255,.55);opacity:0;pointer-events:none}' +
'.sbr-bell{position:relative;flex:none;display:grid;place-items:center;width:2.2em;height:2.2em;border-radius:50%;background:rgba(255,255,255,.1)}' +
'.sbr-bell svg{width:1.25em;height:1.25em;overflow:visible}' +
'.sbr-body{fill:none;stroke:#fff;stroke-width:2;stroke-linejoin:round;transform-origin:12px 3px;transition:fill .2s}' +
'.sbr-rung .sbr-body{fill:#fff}' +
'.sbr-wave{fill:none;stroke:#fff;stroke-width:2;stroke-linecap:round;opacity:0;transform-box:fill-box;transform-origin:center}' +
'.sbr-ptr svg{display:block;width:100%;height:100%}' +
'.sbr-ptr{position:absolute;left:0;top:0;width:1.6em;height:1.6em;margin:-0.15em 0 0 -0.25em;pointer-events:none;opacity:0;filter:drop-shadow(0 0.15em 0.25em rgba(0,0,0,.45))}' +
'.sbr-ptr path{fill:#fff;stroke:#111;stroke-width:1.4;stroke-linejoin:round}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var TICK = '<svg class="sbr-tick" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
var BELL = '<svg viewBox="0 0 24 24" aria-hidden="true">' +
'<path class="sbr-body" d="M12 3a6 6 0 0 0-6 6v4.2L4 17h16l-2-3.8V9a6 6 0 0 0-6-6zM10 19.5a2 2 0 0 0 4 0"/>' +
'<path class="sbr-wave" d="M1.5 6.5a8 8 0 0 1 2.5-3.5"/><path class="sbr-wave" d="M22.5 6.5a8 8 0 0 0-2.5-3.5"/></svg>';
var PTR = '<span class="sbr-ptr" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 2.5v17.2l4.4-4.1 2.9 6.6 3-1.3-2.9-6.5H19z"/></svg></span>';
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function num(el, k) { var v = el.dataset[k]; return v !== undefined && v !== '' ? +v : DEFAULTS[k]; }
function esc(t) { var d = document.createElement('div'); d.textContent = t; return d.innerHTML; }
// Centre of a child, in px from the element's top left (offsets ignore transforms).
function centre(el, child) {
var x = child.offsetWidth / 2, y = child.offsetHeight / 2, n = child;
while (n && n !== el) { x += n.offsetLeft; y += n.offsetTop; n = n.offsetParent; }
return { x: x, y: y };
}
function setup(el) {
var d = el.dataset;
var label = el.textContent.trim() || DEFAULTS.label;
var channel = d.channel || '';
el.classList.add('sbr');
el.style.setProperty('--sbr-c', d.color || DEFAULTS.color);
el.setAttribute('role', 'img');
el.setAttribute('aria-label', (channel ? channel + ': ' : '') + label + ' and turn on notifications');
el.innerHTML =
'<div class="sbr-card" aria-hidden="true">' +
(channel ? '<span class="sbr-av">' + esc(channel.charAt(0).toUpperCase()) + '</span>' +
'<span class="sbr-who"><span class="sbr-name">' + esc(channel) + '</span>' +
(d.count ? '<span class="sbr-count">' + esc(d.count) + '</span>' : '') + '</span>' : '') +
'<span class="sbr-btn">' + TICK + '<span class="sbr-label"></span><span class="sbr-ripple"></span></span>' +
'<span class="sbr-bell">' + BELL + '</span>' +
'</div>' + PTR;
var s = {
label: label, done: d.done || DEFAULTS.done,
card: el.querySelector('.sbr-card'), btn: el.querySelector('.sbr-btn'),
text: el.querySelector('.sbr-label'), ripple: el.querySelector('.sbr-ripple'),
bell: el.querySelector('.sbr-bell'), body: el.querySelector('.sbr-body'),
waves: el.querySelectorAll('.sbr-wave'), ptr: el.querySelector('.sbr-ptr')
};
el._sbr = s;
if (reduced()) { setState(el, true); return; }
setState(el, false);
anime.utils.set(s.card, { opacity: 0, scale: 0.8, y: '0.6em' });
}
function setState(el, on) {
var s = el._sbr;
s.btn.classList.toggle('sbr-on', on);
s.text.textContent = on ? s.done : s.label;
if (!on) s.bell.classList.remove('sbr-rung');
}
function play(el) {
var s = el._sbr;
if (s.tl) s.tl.pause();
clearTimeout(s.wait);
setState(el, false);
var loop = el.dataset.loop !== undefined && el.dataset.loop !== 'false';
var fs = parseFloat(getComputedStyle(el).fontSize) || 16;
var b = centre(el, s.btn), bell = centre(el, s.bell);
var start = { x: el.offsetWidth + fs * 1.5, y: el.offsetHeight + fs * 2.2 };
var away = { x: bell.x + fs * 2.5, y: el.offsetHeight + fs * 2.5 };
anime.utils.set(s.card, { opacity: 0, scale: 0.8, y: '0.6em' });
anime.utils.set(s.ptr, { x: start.x, y: start.y, opacity: 0, scale: 1 });
anime.utils.set(s.ripple, { scale: 0, opacity: 0 });
anime.utils.set(s.btn, { scale: 1 });
anime.utils.set(s.body, { rotate: 0 });
anime.utils.set(s.waves, { opacity: 0, scale: 1 });
var tl = anime.createTimeline();
tl.add(s.card, { opacity: [0, 1], scale: [0.8, 1], y: ['0.6em', '0em'], duration: 600, ease: 'outBack(1.8)' })
// the pointer glides in to the button
.add(s.ptr, { opacity: [0, 1], duration: 250, ease: 'linear' }, 450)
.add(s.ptr, { x: b.x, y: b.y, duration: 1000, ease: 'inOut(3)' }, 450)
// click
.add(s.ptr, { scale: [{ to: 0.82, duration: 110 }, { to: 1, duration: 200 }], ease: 'out(2)' }, 1500)
.add(s.btn, { scale: [{ to: 0.92, duration: 110 }, { to: 1, duration: 420, ease: 'outBack(3)' }] }, 1500)
.add(s.ripple, { scale: [0, 9], opacity: [0.7, 0], duration: 650, ease: 'out(3)' }, 1520)
.call(function () { setState(el, true); }, 1610)
// over to the bell and click it
.add(s.ptr, { x: bell.x, y: bell.y, duration: 620, ease: 'inOut(3)' }, 2050)
.add(s.ptr, { scale: [{ to: 0.82, duration: 110 }, { to: 1, duration: 200 }], ease: 'out(2)' }, 2700)
.add(s.bell, { scale: [{ to: 0.88, duration: 110 }, { to: 1, duration: 380, ease: 'outBack(3)' }] }, 2700)
.call(function () { s.bell.classList.add('sbr-rung'); }, 2790)
.add(s.body, {
rotate: [{ to: -24, duration: 110 }, { to: 20, duration: 150 }, { to: -16, duration: 150 }, { to: 12, duration: 150 }, { to: -7, duration: 150 }, { to: 0, duration: 200 }],
ease: 'inOut(2)'
}, 2790)
.add(s.waves, { opacity: [{ to: 1, duration: 120 }, { to: 1, duration: 500 }, { to: 0, duration: 300 }], scale: [0.6, 1.25], duration: 920, ease: 'out(2)', delay: anime.stagger(60) }, 2800)
// the pointer drifts off, then the card leaves
.add(s.ptr, { x: away.x, y: away.y, opacity: [{ to: 1, duration: 400 }, { to: 0, duration: 300 }], duration: 700, ease: 'in(2)' }, 3500)
.add(s.card, { opacity: 0, scale: 0.9, y: '0.5em', duration: 380, ease: 'in(3)' }, 3700 + num(el, 'hold'));
tl.then(function () {
if (!loop) return;
s.wait = setTimeout(function () { if (s.visible) play(el); else s.pending = true; }, num(el, 'gap'));
});
s.tl = tl;
}
window.animacious = window.animacious || {};
window.animacious.subscribeReminder = function (el) {
if (!el || !el._sbr || reduced()) return;
play(el);
};
function init() {
// Loops wait while the element is off screen and carry on when it returns.
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._sbr;
s.visible = e.isIntersecting;
if (!e.isIntersecting) return;
if (!s.started) { s.started = true; setTimeout(function () { play(e.target); }, 250); }
else if (s.pending) { s.pending = false; play(e.target); }
});
}, { threshold: 0.2 });
document.querySelectorAll('[data-subscribe-reminder]').forEach(function (el) {
if (el._sbr) return;
setup(el);
if (!reduced()) io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-subscribe-reminder data-channel="Pixel Kitchen"
data-count="12.4K subscribers">Subscribe</div>