← Broadcast

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

  1. Paste the snippet just before </head>.
  2. Add data-subscribe-reminder to an element whose text is the button label, and give it your channel name in data-channel. Size it with font-size.

Options

Add any of these attributes to the same element.

data-channelChannel name, shown with an initial badge. Leave it out for just the button and bell.
data-countA small line under the name, such as “12.4K subscribers”.
data-doneButton text after the click. Default “Subscribed”.
data-colorButton colour. Default #ef4444.
data-loopPlay again after a pause, over and over.
data-holdHow long the card stays after the bell rings. Default 1600 ms.
data-gapTime off screen between loops. Default 900 ms.

Call animacious.subscribeReminder(el) to play it again, for example every ten minutes.

1 · Snippet
<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>
2 · Your HTML
<div data-subscribe-reminder data-channel="Pixel Kitchen"
     data-count="12.4K subscribers">Subscribe</div>