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
- Paste the snippet just before
</head>. - Add
data-chat-bubblesto a container with one element per message, each withdata-from="them"ordata-from="me".
Options
Add any of these attributes to the same element.
data-loop | Replay the conversation after a pause. |
data-speed | Typing speed multiplier. Default 1; 2 is twice as fast. |
--chat-me, --chat-them | CSS variables for bubble colours (plus --chat-me-text and --chat-them-text). |
<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><div data-chat-bubbles>
<p data-from="them">Hey! Is it ready?</p>
<p data-from="me">Shipping it now π</p>
</div>