← Buttons

Bell Subscribe

Press subscribe and the bell rings: it swings on its hook with its clapper lagging behind, sound waves flash either side, and it fills in solid as the label turns to “Subscribed”. Press again to unsubscribe and the bell empties out.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-bell-subscribe to a button. The script adds the bell in front of your label.
  3. Listen for its change event; event.detail.subscribed says which way it went.

Options

Add any of these attributes to the same element.

data-onLabel while subscribed. Default “Subscribed”.
data-subscribedStart subscribed.
data-autoplayIt toggles itself every couple of seconds while in view, until someone presses it. Handy for demos.

While subscribed the button has the class bsb-on and aria-pressed="true", so you can restyle it, as the demo does. The bell uses the text colour.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Bell Subscribe: press it and the bell icon rings, swinging on its hook
    // with sound waves either side, and fills in solid as the label changes
    // to "Subscribed". Press again to unsubscribe: the bell empties out.
    // Usage: <button data-bell-subscribe>Subscribe</button>
    // Optional: data-on (label while subscribed, default "Subscribed"),
    // data-subscribed (start subscribed), data-autoplay (it toggles itself
    // every couple of seconds while in view, until someone presses it; handy
    // for demos).
    // Events: fires "change" on the button with event.detail.subscribed.
    // While subscribed the button has the class bsb-on and aria-pressed="true",
    // so you can restyle it.
    var DEFAULTS = { on: 'Subscribed', every: 2600 };
    var BELL = '<svg class="bsb-icon" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
      '<path class="bsb-wave" pathLength="1" d="M3.2 9.5a9.6 9.6 0 0 1 2.4-5"/><path class="bsb-wave" pathLength="1" d="M20.8 9.5a9.6 9.6 0 0 0-2.4-5"/>' +
      '<g class="bsb-bell"><path class="bsb-clapper" d="M10 19.5a2 2 0 0 0 4 0"/>' +
      '<path class="bsb-body" d="M12 3.2a5.6 5.6 0 0 0-5.6 5.6v3.7c0 .9-.4 1.8-1 2.5l-.7.8c-.3.4 0 1 .5 1h13.6c.5 0 .8-.6.5-1l-.7-.8c-.6-.7-1-1.6-1-2.5V8.8A5.6 5.6 0 0 0 12 3.2z" fill="currentColor" fill-opacity="0"/></g></svg>';

    var css =
      '.bsb-icon{width:1.25em;height:1.25em;flex:none;overflow:visible;margin-right:.5em}' +
      '.bsb-bell,.bsb-clapper{transform-box:view-box;transform-origin:12px 2.5px}' +
      '.bsb-wave{stroke-dasharray:1 1;stroke-dashoffset:1;opacity:0;transform-box:fill-box;transform-origin:center}' +
      '.bsb-labels{display:inline-grid}' +
      '.bsb-labels>span{grid-area:1/1;justify-self:center;white-space:nowrap}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function setup(el) {
      var d = el.dataset;
      var labels = document.createElement('span');
      labels.className = 'bsb-labels';
      var off = document.createElement('span');
      while (el.firstChild) off.appendChild(el.firstChild);
      var on = document.createElement('span');
      on.textContent = d.on || DEFAULTS.on;
      labels.appendChild(off);
      labels.appendChild(on);
      el.insertAdjacentHTML('afterbegin', BELL);
      el.appendChild(labels);
      var bell = el.querySelector('.bsb-bell');
      var clapper = el.querySelector('.bsb-clapper');
      var body = el.querySelector('.bsb-body');
      var waves = el.querySelectorAll('.bsb-wave');
      var subscribed = false;

      function show(state, animate) {
        subscribed = state;
        el.classList.toggle('bsb-on', state);
        el.setAttribute('aria-pressed', state ? 'true' : 'false');
        var t = animate && !reduced() ? 1 : 0;
        var a = state ? on : off, b = state ? off : on;
        b.setAttribute('aria-hidden', 'true');
        a.removeAttribute('aria-hidden');
        anime.animate(b, { opacity: 0, y: state ? -8 : 8, duration: 200 * t, ease: 'in(2)' });
        anime.animate(a, { opacity: 1, y: [t ? (state ? 8 : -8) : 0, 0], duration: 300 * t, delay: 100 * t, ease: 'out(3)' });
        anime.animate(body, { fillOpacity: state ? 1 : 0, duration: (state ? 360 : 260) * t, delay: state ? 80 * t : 0, ease: 'out(2)' });
        if (!t || !state) return;
        // Ring: the bell swings on its hook, the clapper lags behind it.
        anime.animate(bell, { rotate: [0, 22, -18, 13, -9, 5, -2, 0], duration: 1000, ease: 'out(1.4)' });
        anime.animate(clapper, { x: [0, -2.2, 2, -1.5, 1, -0.5, 0], duration: 1000, delay: 50, ease: 'out(1.4)' });
        anime.animate(el, { scale: [1, 0.94, 1.05, 1], duration: 460, ease: 'out(2)' });
        anime.animate(waves, {
          strokeDashoffset: [1, 0], opacity: [0, 1, 0], scale: [0.8, 1.15],
          duration: 640, delay: anime.stagger(60, { start: 60 }), ease: 'out(2)'
        });
      }

      show(d.subscribed !== undefined && d.subscribed !== 'false', false);

      var userTouched = false;
      el.addEventListener('click', function (e) {
        if (e.isTrusted) userTouched = true;
        show(!subscribed, true);
        el.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { subscribed: subscribed } }));
      });

      if (!reduced() && d.autoplay !== undefined && d.autoplay !== 'false') {
        var timer = null;
        var loop = function () {
          if (userTouched) return;
          if (!el.matches(':hover')) show(!subscribed, true);
          timer = setTimeout(loop, DEFAULTS.every);
        };
        new IntersectionObserver(function (entries) {
          clearTimeout(timer);
          if (entries[0].isIntersecting) timer = setTimeout(loop, 800);
        }).observe(el);
      }
    }

    function init() {
      document.querySelectorAll('[data-bell-subscribe]').forEach(function (el) {
        if (el._bsb) return;
        el._bsb = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<button data-bell-subscribe>Subscribe</button>

<!-- already subscribed: -->
<button data-bell-subscribe data-subscribed>Subscribe</button>