← Typing

Chat Bubbles

A conversation plays out message by message. The other person “types” with three bouncing dots before each reply, and every bubble pops in with a spring.

Is the new site live?

Going out now πŸš€

Those animations ✨

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-chat-bubbles to a container with one element per message, each with data-from="them" or data-from="me".

Options

Add any of these attributes to the same element.

data-loopReplay the conversation after a pause.
data-speedTyping speed multiplier. Default 1; 2 is twice as fast.
--chat-me, --chat-themCSS variables for bubble colours (plus --chat-me-text and --chat-them-text).
1 Β· Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Chat Bubbles: a conversation plays out message by message. The other person
    // "types" (three bouncing dots) before each reply; bubbles pop in with a spring.
    // Usage:
    //   <div data-chat-bubbles>
    //     <p data-from="them">Hey! Is it ready?</p>
    //     <p data-from="me">Shipping it now πŸš€</p>
    //   </div>
    // Optional: data-loop (replay after a pause), data-speed (typing speed
    // multiplier, default 1). Colours: --chat-me, --chat-them, --chat-me-text,
    // --chat-them-text CSS variables on the container.
    var DEFAULTS = { hold: 2600 };

    var css =
      '[data-chat-bubbles]{display:flex;flex-direction:column;gap:8px}' +
      '.chb{max-width:80%;padding:9px 14px;border-radius:18px;line-height:1.35;margin:0;transform-origin:0 100%}' +
      '.chb.them{align-self:flex-start;background:var(--chat-them,#eceef2);color:var(--chat-them-text,#111);border-bottom-left-radius:6px}' +
      '.chb.me{align-self:flex-end;background:var(--chat-me,#6a0dad);color:var(--chat-me-text,#fff);border-bottom-right-radius:6px;transform-origin:100% 100%}' +
      '.chb-typing{display:inline-flex;gap:4px;align-items:center;padding:12px 14px}' +
      '.chb-typing i{width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.45;display:block}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function setup(el) {
      var msgs = Array.from(el.children).map(function (c) {
        c.classList.add('chb', c.dataset.from === 'me' ? 'me' : 'them');
        return c;
      });
      return { el: el, msgs: msgs };
    }

    function pop(node) {
      node.style.display = '';
      anime.animate(node, { scale: [0.6, 1], opacity: [0, 1], y: [8, 0], duration: 600, ease: 'outBack(1.8)' });
    }

    function play(c) {
      var d = c.el.dataset;
      var speed = +d.speed || 1;
      c.msgs.forEach(function (m) { m.style.display = 'none'; });
      var i = 0;
      (function next() {
        if (i >= c.msgs.length) {
          if (d.loop !== undefined && d.loop !== 'false') {
            setTimeout(function () {
              anime.animate(c.msgs, {
                opacity: 0, y: -10, duration: 400, delay: anime.stagger(40), ease: 'in(2)',
                onComplete: function () { play(c); }
              });
            }, DEFAULTS.hold);
          }
          return;
        }
        var m = c.msgs[i++];
        if (m.classList.contains('me')) {
          return setTimeout(function () { pop(m); next(); }, 700 / speed);
        }
        // Show "typing…" for about as long as the message would take to write.
        var typing = document.createElement('p');
        typing.className = 'chb them chb-typing';
        typing.setAttribute('aria-hidden', 'true');
        typing.innerHTML = '<i></i><i></i><i></i>';
        c.el.insertBefore(typing, m);
        pop(typing);
        var dots = anime.animate(typing.children, {
          y: [{ to: -4, duration: 260, ease: 'out(2)' }, { to: 0, duration: 260, ease: 'in(2)' }],
          opacity: [{ to: 1, duration: 260 }, { to: 0.45, duration: 260 }],
          delay: anime.stagger(140),
          loop: true,
          loopDelay: 200
        });
        var wait = Math.min(1800, Math.max(700, m.textContent.length * 45)) / speed;
        setTimeout(function () {
          dots.pause();
          typing.remove();
          pop(m);
          next();
        }, wait);
      })();
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          play(e.target._chb);
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-chat-bubbles]').forEach(function (el) {
        if (el._chb) return;
        var c = (el._chb = setup(el));
        if (reduced) return;
        c.msgs.forEach(function (m) { m.style.display = 'none'; });
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 Β· Your HTML
<div data-chat-bubbles>
  <p data-from="them">Hey! Is it ready?</p>
  <p data-from="me">Shipping it now πŸš€</p>
</div>