Loading Ellipsis
“Loading” with its dots appearing one by one, then clearing and starting again. The dots are real full stops in your own font, and they keep their space while hidden, so the word never shifts. Prefer a ripple or a hop? There are two more styles.
Loading
Saving your changes
Thinking
How to use it
- Paste the snippet just before
</head>. - Add
data-loading-ellipsisto any bit of text, without the dots:<span data-loading-ellipsis>Loading</span>.
Options
Add any of these attributes to the same element.
data-dots | Number of dots, 2 to 6. Default 3. |
data-speed | Time between dots. Default 400 ms. |
data-style="wave" | All the dots show, fading in and out in a ripple. |
data-style="bounce" | All the dots show, hopping in turn. |
data-char | The character to use for the dots. Default . |
It’s marked as a status and screen readers hear just the text, not the dots. It only moves while it’s on screen. With reduced motion all the dots show.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Loading Ellipsis: "Loading" with its dots appearing one by one, then
// clearing and starting again. The dots are real full stops in your own
// font, and they keep their space while hidden, so the word never shifts.
// It only moves while it's on screen.
// Usage: <span data-loading-ellipsis>Loading</span>
// Optional: data-dots (2 to 6, default 3), data-speed (ms between dots,
// default 400), data-style ("appear", the default: dots pop in one by one;
// "wave": the dots fade in and out in a ripple; "bounce": the dots hop in
// turn), data-char (the character to use, default ".").
var DEFAULTS = { dots: 3, speed: 400, style: 'appear', char: '.' };
var css =
'.lde{white-space:nowrap}' +
'.lde-d{display:inline-block;opacity:0;will-change:transform,opacity}' +
'.lde-still .lde-d{opacity:1}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function build(el) {
var d = el.dataset;
var n = Math.max(2, Math.min(6, Math.round(+d.dots) || DEFAULTS.dots));
var speed = Math.max(80, +d.speed || DEFAULTS.speed);
var mode = d.style || DEFAULTS.style;
var ch = d.char || DEFAULTS.char;
var text = el.textContent.trim() || 'Loading';
el.classList.add('lde');
el.setAttribute('role', 'status');
el.setAttribute('aria-label', text);
el.textContent = '';
var word = document.createElement('span');
word.textContent = text;
word.setAttribute('aria-hidden', 'true');
el.appendChild(word);
var dots = [];
for (var i = 0; i < n; i++) {
var s = document.createElement('span');
s.className = 'lde-d';
s.textContent = ch;
s.setAttribute('aria-hidden', 'true');
el.appendChild(s);
dots.push(s);
}
if (reduced()) { el.classList.add('lde-still'); return null; }
var tl = anime.createTimeline({ loop: true, autoplay: false });
if (mode === 'wave') {
anime.utils.set(dots, { opacity: 0.15 });
dots.forEach(function (dot, i) {
tl.add(dot, { opacity: [{ to: 1, duration: speed, ease: 'out(2)' }, { to: 0.15, duration: speed * 1.5, ease: 'inOut(2)' }] }, i * speed * 0.6);
});
tl.add({ duration: speed * 0.5 });
} else if (mode === 'bounce') {
anime.utils.set(dots, { opacity: 1 });
dots.forEach(function (dot, i) {
tl.add(dot, { y: [{ to: '-0.35em', duration: speed * 0.5, ease: 'out(2)' }, { to: '0em', duration: speed * 0.5, ease: 'in(2)' }] }, i * speed * 0.45);
});
tl.add({ duration: speed });
} else {
// Pop each dot in, hold the full set, then clear them all together.
dots.forEach(function (dot, i) {
tl.add(dot, { opacity: { from: 0, to: 1 }, scale: { from: 0.4, to: 1 }, duration: 160, ease: 'out(3)' }, (i + 1) * speed);
});
tl.add(dots, { opacity: 0, duration: 200, ease: 'out(2)' }, (n + 1) * speed + speed * 0.6);
tl.add({ duration: speed * 0.5 });
}
return tl;
}
function init() {
document.querySelectorAll('[data-loading-ellipsis]').forEach(function (el) {
if (el._lde) return;
el._lde = true;
var tl = build(el);
if (!tl) return;
new IntersectionObserver(function (entries) {
entries[0].isIntersecting ? tl.play() : tl.pause();
}).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><p data-loading-ellipsis>Loading</p>
<!-- a ripple: -->
<span data-loading-ellipsis data-style="wave">Saving your changes</span>