Bouncing Dots
Three dots bounce in turn, each one stretching as it jumps and squashing as it lands. The classic “someone is typing” loader. Size it with font-size and colour it with color.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-bouncing-dots>where the loader should appear.
Options
Add any of these attributes to the same element.
data-dots | Number of dots, 2 to 8. Default 3. |
data-speed | Time for one dot’s bounce. Default 600 ms. |
data-height | How high they jump, in dot widths. Default 1.2. |
data-colors | Comma-separated colours, one per dot, instead of color. |
It only bounces while it’s on screen. With reduced motion the dots sit still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Bouncing Dots: three dots bounce in turn, each one rising, hanging for a
// moment and dropping back with a little squash as it lands. The classic
// "someone is typing" loader. Sized with font-size, coloured with color.
// It bounces while it's on screen.
// Usage: <div data-bouncing-dots aria-label="Loading"></div>
// Optional: data-dots (default 3), data-speed (ms for one dot's bounce,
// default 600), data-height (how high they jump, in dot widths, default 1.2),
// data-colors (comma-separated, one per dot, instead of color).
var DEFAULTS = { dots: 3, speed: 600, height: 1.2 };
var css =
'.bnd{display:inline-flex;align-items:flex-end;gap:0.28em;padding-top:0.6em}' +
'.bnd-dot{width:0.42em;height:0.42em;border-radius:50%;background:currentColor;transform-origin:50% 100%}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function build(el) {
var d = el.dataset;
var n = Math.max(2, Math.min(8, +d.dots || DEFAULTS.dots));
var colors = d.colors ? d.colors.split(',') : [];
el.classList.add('bnd');
el.setAttribute('role', 'progressbar');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
el.textContent = '';
var dots = [];
for (var i = 0; i < n; i++) {
var dot = document.createElement('span');
dot.className = 'bnd-dot';
if (colors.length) dot.style.background = colors[i % colors.length].trim();
el.appendChild(dot);
dots.push(dot);
}
// Leave room above for the jump.
var height = +d.height || DEFAULTS.height;
el.style.paddingTop = 0.42 * height + 0.1 + 'em';
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return null;
var speed = +d.speed || DEFAULTS.speed;
var up = -0.42 * height + 'em';
// Each dot waits its turn, bounces, then rests while the others go, so
// all of them share one cycle and stay in step.
var stagger = speed * 0.32;
var rest = stagger * (n - 1) + speed * 0.5;
return dots.map(function (dot, i) {
return anime.animate(dot, {
y: [
{ to: up, duration: speed * 0.45, ease: 'out(2.5)' },
{ to: '0em', duration: speed * 0.4, ease: 'in(2.5)' },
{ to: '0em', duration: speed * 0.15 }
],
// A stretch on the way up, a squash as it lands.
scaleY: [
{ to: 1.15, duration: speed * 0.2 },
{ to: 1, duration: speed * 0.45 },
{ to: 0.7, duration: speed * 0.2, ease: 'out(2)' },
{ to: 1, duration: speed * 0.15, ease: 'out(2)' }
],
scaleX: [
{ to: 0.9, duration: speed * 0.2 },
{ to: 1, duration: speed * 0.45 },
{ to: 1.25, duration: speed * 0.2, ease: 'out(2)' },
{ to: 1, duration: speed * 0.15, ease: 'out(2)' }
],
delay: i * stagger,
loopDelay: rest,
loop: true,
autoplay: false
});
});
}
function init() {
document.querySelectorAll('[data-bouncing-dots]').forEach(function (el) {
if (el._bnd) return;
el._bnd = true;
var anims = build(el);
if (!anims) return;
// Only bounce while visible, to save battery.
new IntersectionObserver(function (entries) {
anims.forEach(function (a) { entries[0].isIntersecting ? a.play() : a.pause(); });
}).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-bouncing-dots style="font-size: 24px; color: #6a0dad"></div>
<!-- a typing indicator: -->
<div class="bubble"><div data-bouncing-dots aria-label="Sam is typing"></div></div>