Glass Blobs
Soft blobs of frosted glass drift and wobble across a section while bold shapes of colour slide past behind them. Wherever the glass passes over a shape it blurs, brightens and bends it, with a glint of light on each blob’s shoulder.
Clear thinking
How to use it
- Paste the snippet just before
</head>. - Add
data-glass-blobsto a section.
Options
Add any of these attributes to the same element.
data-colors | The shapes behind the glass, comma-separated. Default violet, pink, cyan and amber. |
data-count | How many glass blobs. Default 3 to 5, depending on the section’s size. |
data-blur | How frosted the glass is, in px. Default 14. |
data-speed | Speed multiplier. Default 1. |
data-backdrop="false" | No shapes behind: the glass frosts your own background image instead. |
The glass uses backdrop-filter and everything moves with transforms, so it stays smooth; it pauses when off screen. With reduced motion it holds still.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Glass Blobs: soft blobs of frosted glass drift and wobble across a
// section, blurring and brightening the bold colours that slide past
// behind them, like pebbles of glass over a painted wall. Built from a few
// layers moved on the GPU, and paused when off screen.
// Usage: <section data-glass-blobs style="background:#1e1b4b">…</section>
// Optional: data-colors (the shapes behind the glass, comma-separated,
// default violet, pink, cyan and amber), data-count (how many glass blobs,
// default 3 to 5 depending on size), data-blur (how frosted the glass is,
// in px, default 14), data-speed (default 1), data-backdrop="false" (no
// shapes behind; the glass refracts your own background instead).
var DEFAULTS = { colors: '#8b5cf6,#ec4899,#22d3ee,#f59e0b', blur: 14, speed: 1 };
var css =
'[data-glass-blobs]{position:relative;isolation:isolate;overflow:hidden}' +
'.gls-layer{position:absolute;inset:0;z-index:-1;pointer-events:none;overflow:hidden}' +
'.gls-shape,.gls-glass{position:absolute;left:0;top:0;will-change:transform}' +
'.gls-shape{border-radius:50%}' +
'.gls-glass{overflow:hidden;background:linear-gradient(140deg,rgba(255,255,255,.34),rgba(255,255,255,.06) 55%,rgba(255,255,255,.14));' +
'border:1px solid rgba(255,255,255,.4);box-shadow:inset 0 2px 14px rgba(255,255,255,.45),inset 0 -14px 34px rgba(20,10,60,.18),0 18px 40px rgba(10,5,30,.22)}' +
'.gls-glass::after{content:"";position:absolute;left:16%;top:9%;width:38%;height:22%;border-radius:50%;' +
'background:radial-gradient(closest-side,rgba(255,255,255,.75),rgba(255,255,255,0));transform:rotate(-24deg)}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function build(el) {
var d = el.dataset;
var colors = (d.colors || DEFAULTS.colors).split(',');
var blur = d.blur !== undefined ? +d.blur : DEFAULTS.blur;
var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
var backdrop = d.backdrop !== 'false';
var layer = document.createElement('div');
layer.className = 'gls-layer';
layer.setAttribute('aria-hidden', 'true');
el.insertBefore(layer, el.firstChild);
var R = anime.utils.random;
var shapes = [], blobs = [], w = 0, h = 0, t = R(0, 100);
// Crisp coloured shapes behind, so the glass has something to bend.
if (backdrop) {
for (var i = 0; i < Math.max(4, colors.length + 1); i++) {
var s = document.createElement('div');
s.className = 'gls-shape';
s.style.background = colors[i % colors.length].trim();
if (i % 3 === 2) s.style.borderRadius = '28%';
layer.appendChild(s);
shapes.push({ el: s, k: R(40, 75) / 100, fx: R(5, 12) / 100, fy: R(5, 12) / 100, p: R(0, 628) / 100, q: R(0, 628) / 100, ox: (i + 0.5) / Math.max(4, colors.length + 1) });
}
}
function make(n) {
blobs.forEach(function (b) { b.el.remove(); });
blobs = [];
for (var i = 0; i < n; i++) {
var g = document.createElement('div');
g.className = 'gls-glass';
var f = 'blur(' + blur + 'px) saturate(1.7) brightness(1.12)';
g.style.backdropFilter = f;
g.style.webkitBackdropFilter = f;
layer.appendChild(g);
blobs.push({ el: g, k: R(55, 100) / 100, fx: R(6, 13) / 100, fy: R(6, 13) / 100, p: R(0, 628) / 100, q: R(0, 628) / 100, m: R(0, 628) / 100, ox: (i + 0.5) / n });
}
}
function resize() {
w = el.clientWidth;
h = el.clientHeight;
var n = +d.count || Math.max(3, Math.min(5, Math.round((w * h) / 90000) + 2));
if (n !== blobs.length) make(n);
var m = Math.min(w, h);
shapes.forEach(function (s) { s.size = m * s.k; s.el.style.width = s.el.style.height = s.size + 'px'; });
blobs.forEach(function (b) { b.size = Math.max(70, Math.min(320, m * 0.48 * b.k)); b.el.style.width = b.el.style.height = b.size + 'px'; });
place();
}
// Each piece wanders on slow sine waves around its own lane.
function move(p, z) {
var x = p.ox * w + Math.sin(t * p.fx * z + p.p) * w * 0.32 - p.size / 2;
var y = h * 0.5 + Math.sin(t * p.fy * z + p.q) * (h * 0.5 + p.size * 0.1) - p.size / 2;
return 'translate(' + x.toFixed(1) + 'px,' + y.toFixed(1) + 'px)';
}
function place() {
shapes.forEach(function (s) { s.el.style.transform = move(s, 0.7) + ' rotate(' + (t * 6 * s.fx * 10 + s.p * 40).toFixed(1) + 'deg)'; });
blobs.forEach(function (b) {
b.el.style.transform = move(b, 1);
// Wobble the outline so each blob looks soft, not a plain circle.
var a = [];
for (var i = 0; i < 8; i++) a.push((50 + 13 * Math.sin(t * 0.55 + b.m + i * 1.7)).toFixed(1) + '%');
b.el.style.borderRadius = a[0] + ' ' + a[1] + ' ' + a[2] + ' ' + a[3] + ' / ' + a[4] + ' ' + a[5] + ' ' + a[6] + ' ' + a[7];
});
}
resize();
if (window.ResizeObserver) new ResizeObserver(resize).observe(el);
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var last = 0;
var timer = anime.createTimer({
loop: true,
duration: 1000,
autoplay: false,
onUpdate: function () {
var now = performance.now();
t += ((last ? Math.min(50, now - last) : 16) / 1000) * speed;
last = now;
place();
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
}).observe(el);
}
function init() {
document.querySelectorAll('[data-glass-blobs]').forEach(function (el) {
if (el._gls) return;
el._gls = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><section data-glass-blobs style="background: #1e1b4b; color: #fff">
<h1>Clear thinking</h1>
</section>